unify dns related config, change some dns settings
[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 e sshing $h
1796 ssh $h.b8.nz <<'EOF'
1797 source ~/.bashrc
1798 m dnsup
1799 EOF
1800 done
1801 }
1802 bindpushb8() {
1803 lipush
1804 for h in li bk; do
1805 e sshing $h
1806 ssh $h.b8.nz <<'EOF'
1807 source ~/.bashrc
1808 m dnsb8
1809 EOF
1810 done
1811 }
1812
1813 dnsup() {
1814 conflink -f
1815 m ser reload named
1816 }
1817 dnsb8() {
1818 local f=/var/lib/bind/db.b8.nz
1819 m ser stop named
1820 m sleep 1
1821 m sudo rm -fv $f.jnl $f.signed.jnl
1822 m sudo install -m 644 -o bind -g bind /p/c/machine_specific/vps/bind-initial/db.b8.nz $f
1823 m ser restart named
1824 }
1825 dnsecgen() {
1826 # keys generated like this
1827 # because of https://ftp.isc.org/isc/dnssec-guide/dnssec-guide.pdf
1828 # https://blog.apnic.net/2019/05/23/how-to-deploying-dnssec-with-bind-and-ubuntu-server/
1829
1830 # key length is longer than that guide because
1831 # we are using those at fsf and when old key lengths
1832 # become insecure, I want some extra time to update.
1833 # dnsecgen (in brc2)
1834
1835 local zone=$1
1836 dnssec-keygen -a RSASHA256 -b 2048 $zone
1837 dnssec-keygen -f KSK -a RSASHA256 -b 4096 $zone
1838 for f in K"$zone".*.key; do
1839 # eg Kb8.nz.+008+47995.key tag=47995
1840 # in dnsimple, you add the long string from this.
1841 # in gandi, you add the long string from the .key file,
1842 # then see that the digest matches the ds.
1843 echo "tag is the number after DS"
1844 dnssec-dsfromkey -a SHA-256 $f
1845 done
1846 # For b8.nz, we let bind read the keys and sign, and
1847 # right now they have root ownership, so let them
1848 # get group read.
1849 chmod g+r ./*.private
1850 }
1851 dsign() {
1852 # create .signed file
1853 # note: full paths probably not needed.
1854 local arg
1855 for arg; do
1856 local zone=${arg#db.}
1857 local dir=/p/c/machine_specific/vps/filesystem/var/lib/bind
1858 dnssec-signzone -S -e +31536000 -o $zone -K $dir -d $dir $dir/db.$zone
1859 done
1860 }
1861
1862 # set day start for use in other programs.
1863 # expected to do be in a format like 830, or 800 or 1300.
1864 ds() {
1865 if [[ $1 ]]; then
1866 echo $1 >/b/data/daystart
1867 else
1868 cat /b/data/daystart
1869 fi
1870 }
1871
1872 #### begin bitcoin related things
1873 btc() {
1874 local f=/etc/bitcoin/bitcoin.conf
1875 # importprivkey will timeout if using the default of 15 mins.
1876 # upped it to 1 hour.
1877 bitcoin-cli -rpcclienttimeout=60000 -"$(s grep rpcuser= $f)" -"$(s grep rpcpassword= $f)" "$@"
1878 }
1879 btcusd() { # $1 btc in usd
1880 local price
1881 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1882 printf "$%s\n" "$price"
1883 if [[ $1 ]]; then
1884 printf "$%.2f\n" "$(echo "scale=4; $price * $1"| bc -l)"
1885 fi
1886 }
1887 usdbtc() { # $1 usd in btc
1888 local price
1889 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1890 printf "$%s\n" "$price"
1891 if [[ $1 ]]; then
1892 # 100 mil satoshi / btc. 8 digits after the 1.
1893 printf "%.8f btc\n" "$(echo "scale=10; $1 / $price "| bc -l)"
1894 fi
1895 }
1896 satoshi() { # $1 satoshi in usd
1897 local price
1898 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1899 price=$(echo "scale=10; $price * 0.00000001"| bc -l)
1900 printf "$%f\n" "$price"
1901 if [[ $1 ]]; then
1902 printf "$%.2f\n" "$(echo "scale=10; $price * $1"| bc -l)"
1903 fi
1904 }
1905
1906 # Bitcoin holds open the wallet file. this causes problems for a
1907 # secondary computer running bitcoin and receiving a backup (as of
1908 # 2023). However, in 2024-02, I ran a backup where a receiving machine
1909 # had the wallet enabled and there was no error, so I don't know if this
1910 # is still an issue or likely it is an inconsistent behavior.
1911 #
1912 # As a workaround, this function is for enabling the wallet when I want
1913 # to use it and leave it disabled otherwise.
1914 walleton() {
1915 local active
1916 active=false
1917 no_on=true
1918 if [[ ! $(readlink -f /var/lib/bitcoind/wallets) == /q/wallets ]]; then
1919 if systemctl --quiet is-active bitcoind; then
1920 if [[ -e /tmp/no-bitcoinon ]]; then
1921 no_on=true
1922 else
1923 if [[ $EUID == 0 ]]; then
1924 m install -T -o iank -g iank /dev/null /tmp/no-bitcoinon
1925 else
1926 m touch /tmp/no-bitcoinon
1927 fi
1928 fi
1929 active=true
1930 m ser stop bitcoind
1931 fi
1932 m s ln -s /q/wallets /var/lib/bitcoind
1933 sudo chown -h bitcoin:bitcoin /var/lib/bitcoind/wallets
1934 if $active; then
1935 m ser start bitcoind
1936 if ! $no_on; then
1937 m rm /tmp/no-bitcoinon
1938 fi
1939 fi
1940 fi
1941 }
1942 walletoff() {
1943 local active
1944 active=false
1945 no_on=true
1946 if [[ $(readlink -f /var/lib/bitcoind/wallets) == /q/wallets ]]; then
1947 if systemctl --quiet is-active bitcoind; then
1948 if [[ -e /tmp/no-bitcoinon ]]; then
1949 no_on=true
1950 else
1951 if [[ $EUID == 0 ]]; then
1952 m install -T -o iank -g iank /dev/null /tmp/no-bitcoinon
1953 else
1954 m touch /tmp/no-bitcoinon
1955 fi
1956 fi
1957 active=true
1958 m ser stop bitcoind
1959 else
1960 echo note: bitcoind not active
1961 fi
1962 m rm /var/lib/bitcoind/wallets
1963 if $active; then
1964 # note, starting bitcoin always fails, but it actually
1965 # succeeds. But this is strangely not consistent.
1966 m ser start bitcoind
1967 if ! $no_on; then
1968 m rm /tmp/no-bitcoinon
1969 fi
1970 fi
1971 fi
1972 }
1973 #### end bitcoin related things
1974
1975
1976
1977 cbfstool () { /a/opt/coreboot/build/cbfstool "$@"; }
1978
1979
1980 cgpl()
1981 {
1982 if (($#)); then
1983 cp /a/bin/data/COPYING "$@"
1984 else
1985 cp /a/bin/data/COPYING .
1986 fi
1987 }
1988
1989 capache()
1990 {
1991 if (($#)); then
1992 cp /a/bin/data/LICENSE "$@"
1993 else
1994 cp /a/bin/data/LICENSE .
1995 fi
1996 }
1997
1998
1999
2000 apache-header() {
2001 # First paragraph is to avoid people being confused about why a
2002 # file is apache licensed.
2003 cat <<'EOF'
2004 # I, Ian Kelling, follow the GNU license recommendations at
2005 # https://www.gnu.org/licenses/license-recommendations.en.html. They
2006 # recommend that small programs, < 300 lines, be licensed under the
2007 # Apache License 2.0. This file contains or is part of one or more small
2008 # programs. If a small program grows beyond 300 lines, I plan to switch
2009 # its license to GPL.
2010
2011 # Copyright 2024 Ian Kelling
2012
2013 # Licensed under the Apache License, Version 2.0 (the "License");
2014 # you may not use this file except in compliance with the License.
2015 # You may obtain a copy of the License at
2016
2017 # http://www.apache.org/licenses/LICENSE-2.0
2018
2019 # Unless required by applicable law or agreed to in writing, software
2020 # distributed under the License is distributed on an "AS IS" BASIS,
2021 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2022 # See the License for the specific language governing permissions and
2023 # limitations under the License.
2024
2025 EOF
2026
2027 }
2028
2029 # apply apache to git tracked bash files + README, except files with A?GPL3 header.
2030 apache-apply-repo() {
2031 for f in $(git ls-files); do
2032 [[ -L $f || ! -f $f ]] && continue
2033 if [[ $f != README ]]; then
2034 if ! grep -n '^#!/bin/bash' $f | grep ^1: &>/dev/null; then continue; fi
2035 if head -n 10 $f | grep 'it under the terms of the GNU General Public License as published by' &>/dev/null; then continue; fi
2036 fi
2037 apache-apply $f
2038 done
2039 }
2040
2041 apache-apply() {
2042 for file; do
2043 if head -n1 "$file"| grep -E '^#!/bin/bash\b' &>/dev/null; then
2044 {
2045 head -n1 "$file"
2046 apache-header
2047 tail -n+2 "$file"
2048 } | sponge "$file"
2049 else
2050 {
2051 apache-header
2052 cat "$file"
2053 } | sponge "$file"
2054 fi
2055 done
2056 }
2057 # strip out the apache license from a file.
2058 apache-strip() {
2059 # shellcheck disable=SC2044 # meh
2060 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
2061 }
2062
2063 chrome() {
2064 if type -p chromium &>/dev/null; then
2065 cmd=chromium
2066 else
2067 cd /
2068 cmd="schroot -c bullseye chromium"
2069 CHROMIUM_FLAGS='--enable-remote-extensions' $cmd & r
2070 fi
2071 }
2072
2073
2074 # do all tee.
2075 # pipe to this, or just type like a shell
2076 # todo: test this
2077 dat() {
2078 tee >(ssh frodo.b8.nz) >(ssh x2) >(ssh tp.b8.nz) >(ssh kw) >(ssh tp.b8.nz)
2079 }
2080 da() { # do all
2081 local host
2082 for host in x2 kw tp.b8.nz x3.b8.nz frodo.b8.nz; do
2083 ssh $host "$@"
2084 done
2085 }
2086
2087
2088 debian_pick_mirror () {
2089 # netselect-apt finds a fast mirror.
2090 # but we need to replace the mirrors ourselves,
2091 # because it doesnt do that. best it can do is
2092 # output a basic sources file
2093 # here we get the server it found, get the main server we use
2094 # then substitute all instances of one for the other in the sources file
2095 # and backup original to /etc/apt/sources.list-original.
2096 # this is idempotent. the only way to identify debian sources is to
2097 # note the original server, so we put it in a comment so we can
2098 # identify it later.
2099 local file
2100 file=$(mktemp -d)/f # safe way to get file name without creating one
2101 sudo netselect-apt -o "$file" || return 1
2102 url=$(grep ^\\w $file | head -n1 | awk '{print $2}')
2103 sudo cp -f /etc/apt/sources.list /etc/apt/sources.list-original
2104 sudo sed -ri "/http.us.debian.org/ s@( *[^ #]+ +)[^ ]+([^#]+).*@\1$url\2# http.us.debian.org@" /etc/apt/sources.list
2105 sudo apt-get update
2106 }
2107 digme() {
2108 digdiff @ns{1,2}.iankelling.org "$@"
2109 }
2110
2111 dup() {
2112 local ran_d
2113 ran_d=false
2114 system-status _
2115 case $PS1 in
2116 *[\ \]]D\ *)
2117 pushd /
2118 /b/ds/distro-begin |& ts || return $?
2119 /b/ds/distro-end |& ts || return $?
2120 popd
2121 ran_d=true
2122 ;;&
2123 *[\ \]]DB\ *)
2124 pushd /
2125 /b/ds/distro-begin |& ts || return $?
2126 popd
2127 ran_d=true
2128 ;;
2129 *[\ \]]DE\ *)
2130 pushd /
2131 /b/ds/distro-end |& ts || return $?
2132 popd
2133 ran_d=true
2134 ;;&
2135 *CONFLINK*)
2136 if ! $ran_d; then
2137 conflink
2138 fi
2139 ;;
2140 esac
2141 system-status _
2142 }
2143
2144 envload() { # load environment from a previous: export > file
2145 local file=${1:-$HOME/.${USER}_env}
2146 eval "$(export | sed 's/^declare -x/export -n/')"
2147 while IFS= read -r line; do
2148 # declare -x makes variables local to a function
2149 eval ${line/#declare -x/export}
2150 done < "$file"
2151 }
2152
2153 failfunc() { asdf a b c; }
2154 failfunc2() { failfunc d e f; }
2155
2156 # one that comes with distros is too old for newer devices
2157 fastboot() {
2158 /a/opt/android-platform-tools/fastboot "$@";
2159 }
2160
2161 kdecd() { /usr/lib/x86_64-linux-gnu/libexec/kdeconnectd; }
2162
2163 bat() {
2164 cat /sys/class/power_supply/BAT0/capacity
2165 }
2166
2167 # List of apps to install/update
2168 # Create from existing manually installed apps by doing
2169 # fdroidcl update
2170 # fdroidcl search -i, then manually removing
2171 # automatically installed/preinstalled apps
2172
2173 #
2174 # # my attempt at recovering from boot loop:
2175 # # in that case, boot to recovery (volume up, home button, power, let go of power after samsun logo)
2176 # # then
2177 # mount /dev/block/mmcblk0p12 /data
2178 # cd /data
2179 # find -iname '*appname*'
2180 # rm -rf FOUND_DIRS
2181 # usually good enough to just rm -rf /data/app/APPNAME
2182 #
2183 # currently broken:
2184 # # causes replicant to crash
2185 # org.quantumbadger.redreader
2186 # org.kde.kdeconnect_tp
2187
2188 # not broke, but wont work without gps
2189 #com.zoffcc.applications.zanavi
2190 # not broke, but not using atm
2191 #com.nutomic.syncthingandroid
2192 # # doesn\'t work on replicant
2193 #net.sourceforge.opencamera
2194 #
2195 fdroid_pkgs=(
2196 net.mullvad.mullvadvpn
2197 org.schabi.newpipe
2198 io.github.subhamtyagi.lastlauncher
2199 io.anuke.mindustry
2200 com.biglybt.android.client
2201 de.marmaro.krt.ffupdater
2202 me.ccrama.redditslide
2203 org.fedorahosted.freeotp
2204 at.bitfire.davdroid
2205 com.alaskalinuxuser.justnotes
2206 com.artifex.mupdf.viewer.app
2207 com.danielkim.soundrecorder
2208 com.fsck.k9
2209 com.ichi2.anki
2210 com.jmstudios.redmoon
2211 com.jmstudios.chibe
2212 org.kde.kdeconnect_tp
2213 com.notecryptpro
2214 com.termux
2215 cz.martykan.forecastie
2216 de.danoeh.antennapod
2217 de.blinkt.openvpn
2218 de.marmaro.krt.ffupdater
2219 eu.siacs.conversations
2220 free.rm.skytube.oss
2221 im.vector.alpha # riot
2222 info.papdt.blackblub
2223 me.tripsit.tripmobile
2224 net.gaast.giggity
2225 net.minetest.minetest
2226 net.osmand.plus
2227 org.isoron.uhabits
2228 org.linphone
2229 org.gnu.icecat
2230 org.smssecure.smssecure
2231 org.yaaic
2232 sh.ftp.rocketninelabs.meditationassistant.opensource
2233 )
2234 # https://forum.xda-developers.com/android/software-hacking/wip-selinux-capable-superuser-t3216394
2235 # for maru,
2236 #me.phh.superuser
2237
2238 fdup() {
2239 local -A installed updated
2240 local p
2241 # tried putting this in go buildscript cronjob,
2242 # but it failed with undefined: os.UserCacheDir. I expect its due to
2243 # an environment variable missing, but its easier just to stick it here.
2244 m go get -u mvdan.cc/fdroidcl || return 1
2245 m fdroidcl update
2246 if fdroidcl search -u | grep ^org.fdroid.fdroid; then
2247 fdroidcl install org.fdroid.fdroid
2248 sleep 5
2249 m fdroidcl update
2250 fi
2251 for p in $(fdroidcl search -i| grep -o "^\S\+"); do
2252 installed[$p]=true
2253 done
2254 for p in $(fdroidcl search -u| grep -o "^\S\+"); do
2255 updated[$p]=false
2256 done
2257 for p in ${fdroid_pkgs[@]}; do
2258 if ! ${installed[$p]:-false}; then
2259 m fdroidcl install $p
2260 # sleeps are just me being paranoid since replicant has a history of crashing when certain apps are installed
2261 sleep 5
2262 fi
2263 done
2264 for p in ${!installed[@]}; do
2265 if ! ${updated[$p]:-true}; then
2266 m fdroidcl install $p
2267 sleep 5
2268 fi
2269 done
2270 }
2271
2272 firefox-default-profile() {
2273 local key value section
2274 key=Default
2275 value=1
2276 section=$1
2277 file=/p/c/subdir_files/.mozilla/firefox/profiles.ini
2278 sed -ri "/^ *$key/d" "$file"
2279 sed -ri "/ *\[$section\]/,/^ *\[[^]]+\]/{/^\s*${key}[[:space:]=]/d};/ *\[$section\]/a $key=$value" "$file"
2280 }
2281 fdhome() { #firefox default home profile
2282 firefox-default-profile Profile0
2283 }
2284
2285 fdwork() {
2286 firefox-default-profile Profile4
2287 }
2288
2289 ff() {
2290 if type -P firefox &>/dev/null; then
2291 firefox "$@"
2292 else
2293 iceweasel "$@"
2294 fi
2295 }
2296
2297 fn() {
2298 firefox -P alt "$@" >/dev/null 2>&1
2299 }
2300
2301
2302 fsdiff () {
2303 local missing=false
2304 local dname="${PWD##*/}"
2305 local m="/a/tmp/$dname-missing"
2306 local d="/a/tmp/$dname-diff"
2307 [[ -e $d ]] && rm "$d"
2308 [[ -e $m ]] && rm "$m"
2309 local msize=0
2310 local fsfile
2311 while read -r line; do
2312 fsfile="$1${line#.}"
2313 if [[ -e "$fsfile" ]]; then
2314 md5diff "$line" "$fsfile" && tee -a "/a/tmp/$dname-diff" <<< "$fsfile $line"
2315 else
2316 missing=true
2317 echo "$line" >> "$m"
2318 msize=$((msize + 1))
2319 fi
2320 done < <(find . -type f )
2321 if $missing; then
2322 echo "$m"
2323 (( msize <= 100 )) && cat $m
2324 fi
2325 }
2326 fsdiff-test() {
2327 local tmpd x
2328 # expected output, with different tmp dirs
2329 # /tmp/tmp.HDPbwMqdC9/c/d ./c/d
2330 # /a/tmp/tmp.qLDkYxBYPM-missing
2331 # ./b
2332 tmpd="$(mktemp -d)"
2333 cd "$tmpd"
2334 echo ok > a
2335 echo nok > b
2336 mkdir c
2337 echo ok > c/d
2338 local x
2339 x=$(mktemp -d)
2340 mkdir $x/c
2341 echo different > $x/c/d
2342 echo ok > $x/a
2343 fsdiff $x
2344 rm -r "$x" "$tmpd"
2345 }
2346 rename-test() {
2347 # test whether missing files were renamed, generally for use with fsdiff
2348 # $1 = fsdiff output file, $2 = directory to compare to. pwd = fsdiff dir
2349 # echos non-renamed files
2350 local x y found
2351 unset sums
2352 for x in "$2"/*; do
2353 { sums+=( "$(md5sum < "$x")" ) ; } 2>/dev/null
2354 done
2355 while read -r line; do
2356 { missing_sum=$(md5sum < "$line") ; } 2>/dev/null
2357 renamed=false
2358 for x in "${sums[@]}"; do
2359 if [[ $missing_sum == "$x" ]]; then
2360 renamed=true
2361 break
2362 fi
2363 done
2364 $renamed || echo "$line"
2365 done < "$1"
2366 return 0
2367 }
2368
2369 feh() {
2370 # F = fullscren, z = random, Z = auto zoom
2371 command feh --auto-rotate -FzZ "$@"
2372 }
2373
2374
2375
2376 fw() {
2377 firefox -P default "$@" >/dev/null 2>&1
2378 }
2379
2380 gitian() {
2381 git config user.email ian@iankelling.org
2382 }
2383
2384 # at least in flidas, things rely on gpg being gpg1
2385 gpg() {
2386 if type -P gpg2 &>/dev/null; then
2387 command gpg2 "$@"
2388 else
2389 command gpg "$@"
2390 fi
2391 }
2392
2393 gse() {
2394 local email=iank@fsf.org
2395 git send-email --notes "--envelope-sender=<$email>" \
2396 --suppress-cc=self "$@"
2397 }
2398
2399 gup() { /a/f/gnulib/build-aux/gnupload "$@"; }
2400
2401 dejagnu() { /a/opt/dejagnu/dejagnu "$@"; }
2402
2403 hstatus() {
2404 # do git status on published repos.
2405 c /a/bin/githtml
2406 for x in *; do
2407 cd "$(readlink -f $x)"/..
2408 status=$(i status -s) || pwd
2409 if [[ $status ]]; then
2410 hr
2411 echo $x
2412 printf "%s\n" "$status"
2413 fi
2414 cd /a/bin/githtml
2415 done
2416 }
2417
2418 ## work log
2419 #
2420 # note: database location is specified in ~/.timetrap.yml, currently /p/.timetrap.db
2421 wlog() {
2422 local day i days_back
2423 days_back=${1:-16}
2424 for (( i=days_back; i>=0; i-- )); do
2425 day=$( date +%F -d @$((EPOCHSECONDS - 86400*i )) )
2426 date "+%a %b %d" -d @$((EPOCHSECONDS - 86400*i )) | tr '\n' ' '
2427 /a/opt/timetrap/bin/t d -ftotal -s $day -e $day all -m '^w|lunch$'
2428 done
2429 }
2430 to() { t out -a "$@"; }
2431 ti() { t in -a "$@"; }
2432 tl() {
2433 local in_secs
2434 to "$*"
2435 t s lunch
2436 t in -a "$*"
2437 in_secs="$(date -d "${*//[_.]/ }" +%s)"
2438 m t out -a "$(date +%F.%T -d @$(( in_secs + 60*45 )) )"
2439 t s w
2440 }
2441
2442
2443 # help me focus. opens 2 windows.
2444 focus() {
2445 /p/c/proc/focus/linux-amd64/focus &
2446 watcharb5
2447 kill %%
2448 }
2449
2450
2451 # Display a list of the active window title
2452 # i've been on with 10 second samples going back
2453 # 5 minutes. If I've been on one window for 10 seconds
2454 # or longer, then display the second count.
2455 #
2456 # Press any key to exit.
2457 watcharb5() {
2458 local char ret
2459 killall arbtt-capture &>/dev/null ||:
2460 rm -f ~/.arbtt/capture.log
2461 arbtt-capture --sample-rate=10 &
2462 while true; do
2463 arb5
2464 ret=0
2465 # i first thought to sleep and capture ctrl-c, but it seems we can't
2466 # capture control-c, unless maybe we implement the commands in a
2467 # separate script or maybe add err-cleanup to err. Anyways, this
2468 # method is superior because any single char exits.
2469 read -rsN1 -t 5 char || ret=$?
2470 if (( ret == 142 )) || [[ ! $char ]]; then
2471 # debug
2472 #e ret=$ret char=$char
2473 :
2474 else
2475 killall arbtt-capture ||:
2476 return 0
2477 fi
2478 done
2479
2480 }
2481
2482 arb5() {
2483 local i j l sec blanks line
2484 local -a arbtt_lines
2485 if [[ ! -e ~/.arbtt/capture.log ]]; then
2486 sleep 5
2487 fi
2488 blanks=$(( LINES - 34 ))
2489 for (( i=0; i < blanks; i++ )); do
2490 echo
2491 done
2492
2493 {
2494 i=0
2495 j=0
2496 # https://stackoverflow.com/questions/56486272/how-to-concat-multiple-fields-to-same-line-with-jq
2497 arbtt_lines=$(arbtt-dump -l 30 -t json | \
2498 jq -r '.[] | [ ( .inactive / 1000 | floor ) , ( .windows[] | select (.active == true) |.title) ] | @tsv' | tac)
2499 for line in "${arbtt_lines[@]}"; do
2500 read -r sec l <<<"$line"
2501 if (( j >= LINES )); then
2502 break
2503 fi
2504 if (( i % 6 == 0 && i >= 2 )); then
2505 j=$(( j + 1 ))
2506 echo "## $(( i / 6 + 1 )) ##"
2507 fi
2508 if (( sec > 10 )); then
2509 printf "%3d %s\n" $sec "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2510 else
2511 printf " %s\n" "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2512 fi
2513 i=$(( i + 1 ))
2514 j=$(( j + 1 ))
2515 done
2516 while (( j < 34 && j < LINES )); do
2517 echo
2518 j=$(( j + 1 ))
2519 done
2520 } | tac
2521 }
2522
2523 arbttlog() {
2524 # from the log, show only the currently active window, and the number of
2525 # seconds of input inactivity.
2526 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}' ; }
2527
2528 idea() {
2529 /a/opt/idea-IC-163.7743.44/bin/idea.sh "$@" & r
2530 }
2531
2532 ilogs-local() {
2533 d=/var/lib/znc/moddata/log/iank/
2534 for n in freenode libera; do
2535 cd $d/$n
2536 hr
2537 for x in "#$1/"*; do
2538 base=${x##*/}
2539 files=()
2540 for f in $@; do
2541 tmp=\#$f/$base
2542 if [[ -e $tmp ]]; then
2543 files+=(\#$f/$base)
2544 fi
2545 done
2546 sed \"s/^./${base%log}/\" ${files[@]}|sort -n
2547 hr
2548 done
2549 done
2550 }
2551 ilogs() {
2552 sl root@iankelling.org ilogs-local "$@"
2553 }
2554
2555
2556 ilog-local() {
2557 local d chan
2558 chan="$1"
2559 d=/var/lib/znc/moddata/log/iank/
2560 for n in freenode libera; do
2561 if [[ ! -d $d$n/"$chan" ]]; then
2562 continue
2563 fi
2564 cd $d$n/"$chan"
2565 hr
2566 for x in *; do
2567 # *** are parts and joins and such, and they make reading hard.
2568 # I probably will want to see them sometimes, just have to
2569 # remove that part.
2570 echo $x; sed "s/^./${x%log}/;/\*\*\*/d" $x; hr;
2571 done
2572 done
2573 }
2574 ilog() {
2575 local chan
2576 chan="${1:-#fsfsys}"
2577 # use * instead of -r since that does sorted order
2578 sl root@iankelling.org ilog-local "$chan" | less +G
2579 }
2580
2581 o() {
2582 if type gio &> /dev/null ; then
2583 gio open "$@"
2584 elif type gvfs-open &> /dev/null ; then
2585 gvfs-open "$@"
2586 else
2587 xdg-open "$@"
2588 fi
2589 # another alternative is run-mailcap
2590 }
2591 ccomp xdg-open o
2592
2593 # jfilter() {
2594 # grep -Evi -e "^(\S+\s+){4}(sudo|sshd|cron)\[\S*:" \
2595 # -e "^(\S+\s+){4}systemd\[\S*: (starting|started) (btrfsmaintstop|dynamicipupdate|spamd dns bug fix cronjob|rss2email)\.*$"
2596 # }
2597 # jtail() {
2598 # journalctl -n 10000 -f "$@" | jfilter
2599 # }
2600 # jr() { journalctl "$@" | jfilter | less ; }
2601 # jrf() { journalctl -n 200 -f "$@" | jfilter; }
2602
2603
2604 ## old version for model01. i need to get that firmware working again.
2605 # kff() { # keyboardio firmware flash. you must hold down the tilde key
2606 # pushd /a/opt/Model01-Firmware
2607 # # if we didn't want this yes hack, then remove "shell read" from
2608 # # /a/opt/Kaleidoscope/etc/makefiles/sketch.mk
2609 # yes $'\n' | VERBOSE=1 make flash
2610 # popd
2611 # }
2612
2613
2614 kff() {
2615 pushd /a/opt/Kaleidoscope/examples/Devices/Keyboardio/Model100
2616 make flash
2617 popd
2618 }
2619
2620 wgkey() {
2621 local umask_orig name
2622 if (( $# != 1 )); then
2623 e expected 1 arg >&2
2624 return 1
2625 fi
2626 name=$1
2627 umask_orig=$(umask)
2628 umask 0077
2629 wg genkey | tee $name-priv.key | wg pubkey > $name-pub.key
2630 umask $umask_orig
2631 }
2632
2633 host-info-update() {
2634
2635 local -A vpn_ips host_ips host_macs nonvpn_ips
2636 local -a root_hosts nonroot_hosts
2637
2638 # the hosts with no mac
2639 root_hosts=( bk je li b8.nz )
2640 for h in ${root_hosts[@]}; do
2641 root_hosts+=(${h}ex)
2642 done
2643 root_hosts+=(cmc)
2644
2645 while read -r ip host mac opts; do
2646 if [[ $ip == *#* || ! $host ]]; then continue; fi
2647 if [[ $opts == vpn ]]; then
2648 vpn_ips[$host]=$ip
2649 else
2650 nonvpn_ips[$host]=$ip
2651 fi
2652
2653
2654 if [[ $opts == user=root ]]; then
2655 root_hosts+=($host i$host)
2656 else
2657 nonroot_hosts+=($host i$host)
2658 fi
2659
2660 host_ips[$host]=$ip
2661 host_macs[$host]=$mac
2662 done </p/c/host-info
2663
2664 cedit /p/c/subdir_files/.ssh/config <<EOF || [[ $? == 1 ]]
2665 Host ${nonroot_hosts[@]}
2666 User iank
2667 IdentityFile ~/.ssh/home
2668
2669 Host ${root_hosts[@]}
2670 IdentityFile ~/.ssh/home
2671 EOF
2672
2673
2674 grep -E '^[a-z0-9]+[[:space:]]' /p/c/machine_specific/vps/bind-initial/db.b8.nz | awk '{print $1,$3}'
2675
2676 local host ipsuf f files
2677
2678 sedi '/edits below here are made automatically/,$d' /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf
2679 for host in ${!vpn_ips[@]}; do
2680 ipsuf=${vpn_ips[$host]}
2681 wghole $host $ipsuf
2682 u /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service <<EOF
2683 [Unit]
2684 Description=OpenVPN tunnel for %I
2685 After=syslog.target network-online.target
2686 Wants=network-online.target
2687 Documentation=man:openvpn(8)
2688 Documentation=https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
2689 Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
2690 Requires=iptables.service
2691
2692 [Service]
2693 Type=notify
2694 RuntimeDirectory=openvpn-client
2695 RuntimeDirectoryMode=0710
2696 WorkingDirectory=/etc/openvpn/client
2697 ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config /etc/openvpn/client/%i.conf
2698 # todo, try reenabling this from the default openvpn,
2699 # it was disabled so we could do bind mounts as a command,
2700 # but now systemd handles it
2701 #CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE
2702 LimitNPROC=10
2703 # DeviceAllow=/dev/null rw
2704 # DeviceAllow=/dev/net/tun rw
2705
2706 # we use .1 to make this be on a different network than kd, so that we can
2707 # talk to transmission on kd from remote host, and still use this
2708 # vpn.
2709 ExecStartPre=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns -n 10.174.$ipsuf start %i
2710 ExecStartPre=/sbin/iptables-restore /a/bin/distro-setup/transmission-firewall/netns.rules
2711 # allow wireguard network to connect
2712 ExecStartPre=/usr/sbin/ip r add 10.8.0.0/24 via 10.174.$ipsuf.1 dev veth1-client
2713 ExecStopPost=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns stop %i
2714 PrivateNetwork=true
2715 BindReadOnlyPaths=/etc/tr-resolv:/run/systemd/resolve:norbind /etc/basic-nsswitch:/etc/resolved-nsswitch:norbind
2716
2717 [Install]
2718 WantedBy=multi-user.target
2719 EOF
2720 done
2721
2722 {
2723 echo "cat <<EOF"
2724 for host in ${!host_ips[@]}; do
2725 ipsuf=${host_ips[$host]}
2726 echo 'local-data-ptr: "$l.'$ipsuf $host.b8.nz'"'
2727 done
2728 echo "EOF"
2729 } | u /p/ptr-data
2730
2731 {
2732 echo "cat <<EOF"
2733 for host in ${!host_ips[@]}; do
2734 ipsuf=${host_ips[$host]}
2735 echo "dhcp-host=${host_macs[$host]},set:$host,\$l.7,$host"
2736 done
2737 echo "EOF"
2738 } | u /p/dnsmaq-data
2739
2740 {
2741 for host in ${!nonvpn_ips[@]}; do
2742 ipsuf=${nonvpn_ips[$host]}
2743 echo "$host A 10.2.0.$ipsuf"
2744 done
2745 for host in ${!vpn_ips[@]}; do
2746 ipsuf=${vpn_ips[$host]}
2747 cat <<EOF
2748 $host A 10.2.0.$ipsuf
2749 ${host}wg A 10.8.0.$ipsuf
2750 ${host}vp A 10.5.5.$ipsuf
2751 ${host}tr A 10.174.$ipsuf.2
2752 EOF
2753 done
2754 } | cedit vpn-ips-update /p/c/machine_specific/vps/bind-initial/db.b8.nz ||:
2755
2756
2757 echo checking for stray files:
2758
2759 initial_dir=$PWD
2760 cd /a/bin/ds/machine_specific
2761 ngset
2762 files=( */filesystem/etc/systemd/system/openvpn-client-tr@.service )
2763 ngreset
2764 cd $initial_dir
2765 for f in "${files[@]}"; do
2766 host=${f%%/*}
2767 if [[ ! ${vpn_ips[$host]} ]]; then
2768 e /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service
2769 fi
2770 done
2771
2772 cd /p/c/machine_specific
2773 ngset
2774 files=( */filesystem/etc/wireguard/wghole.conf )
2775 ngreset
2776 cd $initial_dir
2777 for f in "${files[@]}"; do
2778 host=${f%%/*}
2779 if [[ ! ${vpn_ips[$host]} ]]; then
2780 e rm /p/c/machine_specific/$host/filesystem/etc/wireguard/wghole.conf
2781 fi
2782 done
2783 }
2784
2785 # usage host ipsuf [extrahost]
2786 #
2787 # If the keys already exist and you want new ones, remove them:
2788 # rm /p/c/machine_specific/$host/filesystem/etc/wireguard/hole-{priv,pub}.key
2789 #
2790 # extrahost is a host/cidr that is allowed to go be routed through the
2791 # vpn by this host.
2792 wghole() {
2793 if (( $# < 2 || $# > 3 )); then
2794 e expected 2-3 arg of hostname, ip suffix, and extrahost >&2
2795 return 1
2796 fi
2797 local host ipsuf umask_orig vpn_allowed
2798 host=$1
2799 ipsuf=$2
2800 if [[ $3 ]]; then
2801 extrahost=,$3
2802 fi
2803 for vpn_host in ${!vpn_ips[@]}; do
2804 if [[ $vpn_host == "$host" ]]; then
2805 continue
2806 fi
2807 vpn_allowed+=",10.174.${vpn_ips[$vpn_host]}.2/32"
2808 done
2809 mkdir -p /p/c/machine_specific/$host/filesystem/etc/wireguard
2810 (
2811 cd /p/c/machine_specific/$host/filesystem/etc/wireguard
2812 umask_orig=$(umask)
2813 umask 0077
2814 if [[ ! -s hole-priv.key || ! -s hole-pub.key ]]; then
2815 wg genkey | tee hole-priv.key | wg pubkey > hole-pub.key
2816 fi
2817 cat >wghole.conf <<EOF
2818 [Interface]
2819 # contents hole-priv.key
2820 PrivateKey = $(cat hole-priv.key)
2821 ListenPort = 1194
2822 Address = 10.8.0.$ipsuf/24
2823 # https://dev.to/tangramvision/what-they-don-t-tell-you-about-setting-up-a-wireguard-vpn-1h2g
2824 # ||: makes the systemd service not fail due to the failed command
2825 PostUp = ping -w10 -c1 10.8.0.1 ||:
2826
2827 [Peer]
2828 # li. called wgmail on that server
2829 PublicKey = CTFsje45qLAU44AbX71Vo+xFJ6rt7Cu6+vdMGyWjBjU=
2830 AllowedIPs = 10.8.0.0/24$vpn_allowed$extrahost
2831 Endpoint = 72.14.176.105:1194
2832 PersistentKeepalive = 25
2833 EOF
2834 umask $umask_orig
2835 # old approach. systemd seems to work fine and cleaner.
2836 rm -f ../network/interfaces.d/wghole
2837 cedit -q $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf <<EOF || [[ $? == 1 ]]
2838 [Peer]
2839 PublicKey = $(cat hole-pub.key)
2840 AllowedIPs = 10.8.0.$ipsuf/32,10.174.${vpn_ips[$host]}.2/32
2841 EOF
2842 )
2843 }
2844
2845
2846 mns() { # mount namespace
2847 ns=$1
2848 shift
2849 s mkdir -p /root/mount_namespaces
2850 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2851 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2852 fi
2853 m sudo mount --make-private /root/mount_namespaces
2854 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2855 m sudo touch /root/mount_namespaces/$ns
2856 fi
2857 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2858 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2859 fi
2860 m sudo -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
2861 }
2862
2863 mnsd() { # mount namespace + systemd namespace
2864 ns=$1
2865 unit=$2
2866 shift 2
2867
2868 s mkdir -p /root/mount_namespaces
2869 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2870 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2871 fi
2872 m sudo mount --make-private /root/mount_namespaces
2873 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2874 m sudo touch /root/mount_namespaces/$ns
2875 fi
2876 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2877 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2878 fi
2879
2880 pid=$(servicepid $unit)
2881 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
2882 export -p >$tmpf
2883 printf "%s " "${@@Q}" >>$tmpf
2884 echo >>$tmpf
2885
2886 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
2887 }
2888
2889
2890 mnsr() { # mns run
2891 local ns=$1
2892 shift
2893 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
2894 }
2895
2896 mnsnonetr() {
2897 ns=$1
2898 lomh
2899 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2900 s ip netns add nonet
2901 fi
2902 mns $ns --net=/var/run/netns/nonet /bin/bash
2903 lomh
2904 }
2905
2906 mnsnonet() {
2907 ns=$1
2908 lomh
2909 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2910 s ip netns add nonet
2911 fi
2912 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
2913 lomh
2914 }
2915
2916
2917 lom() {
2918 # l = the loopback device
2919 local l base
2920 # get sudo pass cached right away
2921 if ! sudo -nv 2>/dev/null; then
2922 sudo -v
2923 fi
2924 if [[ $1 == /* ]]; then
2925 base=${1##*/}
2926 fs_file=$1
2927 if mns $base mountpoint -q /mnt/$base; then
2928 return 0
2929 fi
2930 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
2931 if [[ ! $l ]]; then
2932 l=$(sudo losetup -f)
2933 m sudo losetup $l $fs_file
2934 fi
2935 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2936 if ! m sudo cryptsetup luksOpen $l $base; then
2937 m sudo losetup -d $l
2938 return 1
2939 fi
2940 fi
2941 m sudo mkdir -p /mnt/$base
2942 m mns $base mount /dev/mapper/$base /mnt/$base
2943 m mns $base chown $USER:$USER /mnt/$base
2944 lomh
2945 else
2946 base=$1
2947 if mns $base mountpoint /mnt/$base &>/dev/null; then
2948 m mns $base umount /mnt/$base
2949 fi
2950 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2951 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
2952 echo lom: failed cryptsetup luksClose /dev/mapper/$base
2953 return 1
2954 fi
2955 fi
2956 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
2957 if [[ $l ]]; then
2958 m sudo losetup -d $l
2959 else
2960 echo lom: warning: no loopback device found
2961 fi
2962 fi
2963 }
2964
2965 # mu personality. for original, just run mp. for 2, run mp 2.
2966 # this is partly duplicated in mail-setup
2967 mp() {
2968 local dead=false
2969 for s in {1..5}; do
2970 if ! killall mu; then
2971 dead=true
2972 break
2973 fi
2974 sleep 1
2975 done
2976 if ! $dead; then
2977 echo error: mu not dead
2978 m psg mu
2979 return 1
2980 fi
2981 suf=$1
2982 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
2983 while (($#)); do
2984 target=$1$suf
2985 f=$2
2986 shift 2
2987 if [[ -e $f && ! -L $f ]]; then
2988 m rm -rf $f
2989 fi
2990 m ln -sf -T $target $f
2991 done
2992 }
2993
2994 # maildir enable
2995 mdenable() {
2996 local md dst ln_path src two
2997
2998 two=false
2999 case $1 in
3000 -2) two=true; shift ;;
3001 esac
3002
3003 for md; do
3004 src=
3005 if $two; then
3006 dst=/m/4e2/$md
3007 else
3008 dst=/m/4e/$md
3009 fi
3010
3011 ln_path=/m/md/$md
3012 for d in /m/md/$md /m/4e2/$md; do
3013 if [[ -d $d && ! -L $d ]]; then
3014 src=$d
3015 break
3016 fi
3017 done
3018 if [[ ! $src ]]; then
3019 echo "error: could not find $md" >&2
3020 return 1
3021 fi
3022 m mv -T $src $dst
3023 m ln -sf -T $dst $ln_path
3024 done
3025 }
3026 md2enable() {
3027 mdenable -2 "$@"
3028 }
3029 mddisable() {
3030 local md=$1
3031 dst=/m/md/$md
3032
3033 ### begin copied from mdenable, but different d ###
3034 for d in /m/4e/$md /m/4e2/$md; do
3035 if [[ -d $d && ! -L $d ]]; then
3036 src=$d
3037 break
3038 fi
3039 done
3040 if [[ ! $src ]]; then
3041 echo "error: could not find $md" >&2
3042 return 1
3043 fi
3044 ### end copy from mdenable ###
3045
3046 if [[ -L $dst ]]; then m rm $dst; fi
3047 m mv -T $src $dst
3048 }
3049
3050
3051 mdt() {
3052 markdown "$1" >/tmp/mdtest.html
3053 firefox /tmp/mdtest.html
3054 }
3055
3056 mo() { xset dpms force off; } # monitor off
3057
3058 mpvgpu() {
3059 # seems to be the best gpu decoding on my nvidia 670.
3060 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3061
3062
3063 case $HOSTNAME in
3064 kd)
3065 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3066 ;;
3067 esac
3068 # going back to the default slow clock, and slower fan:
3069 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3070 if [[ $DISPLAY ]]; then
3071 mpv --vo=vdpau --hwdec=auto "$@"
3072 else
3073 # waylandvk seems to work the same
3074 mpv --gpu-context=wayland --hwdec=auto
3075 fi
3076 }
3077
3078 mpvd() {
3079 mpv --profile=d "$@";
3080 }
3081 mpva() {
3082 mpv --profile=a "$@";
3083 }
3084 # mpv all media files in . or $1
3085 mpvm() {
3086 local -a extensions arg
3087 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3088 # into /a/x.log, then
3089 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3090
3091 # note: to join them together for a regex, do:
3092 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3093 extensions=(
3094 .webm
3095 .mkv
3096 .flv
3097 .flv
3098 .vob
3099 .ogv .ogg
3100 .drc
3101 .gif
3102 .gifv
3103 .mng
3104 .avi
3105 .MTS .M2TS .TS
3106 .mov .qt
3107 .wmv
3108 .yuv
3109 .rm
3110 .rmvb
3111 .viv
3112 .asf
3113 .amv
3114 .mp4 .m4p .m4v
3115 .mpg .mp2 .mpeg .mpe .mpv
3116 .mpg .mpeg .m2v
3117 .m4v
3118 .svi
3119 .3gp
3120 .3g2
3121 .mxf
3122 .roq
3123 .nsv
3124 )
3125 arg=("(" -iname "*${extensions[0]}")
3126 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3127 arg+=(-o -iname "*${extensions[i]}")
3128 done
3129 arg+=(")")
3130 dir=${1:-.}
3131 # debug:
3132 #find $dir "${arg[@]}" -size +200k
3133 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3134 }
3135 mpvs() {
3136 mpv --profile=s "$@";
3137 }
3138
3139 myirc() {
3140 if [[ ! $1 ]]; then
3141 set -- fsfsys
3142 fi
3143 local -a d
3144 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3145 # use * instead of -r since that does sorted order
3146 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3147 }
3148
3149
3150 allmyirc() {
3151 local d
3152 d=/var/lib/znc/moddata/log/iank/freenode
3153 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3154 }
3155
3156 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3157 # is that there are sometimes duplicates, and sometimes the a log file
3158 # is for a specific day yet logs messages for subsequent days, and the
3159 # only way to realize that is to notice that the timestamps rolled over
3160 # into a new day, you can't see it in isolation. So, basically, pidgin
3161 # logs are really annoying to read a grep of my messages to find the
3162 # date and time I said when I started and stopped working, so I'm trying
3163 # out a new client: profanity.
3164 mypidgin() {
3165 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3166 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3167 # shellcheck disable=SC2016 # false positive on ${
3168 grep -A1 ') iank:' ./*.txt \
3169 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3170 s/^[^ ]*\.txt-//
3171 /^--$/d
3172 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3173 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3174 }
3175
3176 # my profanity
3177 #
3178 myprof() {
3179 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3180 logs=(*)
3181 logcount=${#logs[@]}
3182 if (( logcount > 15 )); then
3183 i=$(( logcount - 15 ))
3184 else
3185 i=0
3186 fi
3187 # usually do this on monday, sometimes later
3188 if [[ $(date +%A) == Monday ]]; then
3189 min_date=$(date -d 'monday 2 weeks ago' +%s)
3190 else
3191 min_date=$(date -d 'monday 3 weeks ago' +%s)
3192 fi
3193 for (( ; i < logcount; i++ )); do
3194 log=${logs[$i]}
3195 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3196 if (( d < min_date )); then
3197 continue
3198 fi
3199 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3200 hr
3201 fi
3202 done
3203 popd
3204 }
3205
3206
3207 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3208 # especially for links.
3209 profr() {
3210 case $HOSTNAME in
3211 kd)
3212 profr-local
3213 ;;
3214 *)
3215 ssh b8.nz profr-local
3216 ;;
3217 esac
3218 }
3219
3220 profr-local() {
3221 local d0 d1
3222 local -a files
3223 d0="$(date +%Y_%m_%d).log"
3224 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3225 ngset
3226 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3227 ngreset
3228 if (( ${#files[@]} > 0 )); then
3229 cat "${files[@]}" | sort | tail -n 40
3230 fi
3231 }
3232
3233
3234 # Tail pms in the last day, for the case where we restart profanity and
3235 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3236 # If that isn't the case, use prof-recent-local
3237 prof-recent() {
3238 case $HOSTNAME in
3239 kd)
3240 prof-recent-local
3241 ;;
3242 *)
3243 ssh b8.nz prof-recent-local
3244 ;;
3245 esac
3246 }
3247 prof-recent-local() {
3248 local d dates date files f
3249 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3250 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3251 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3252 files=()
3253 for date in ${dates[@]}; do
3254 f=$d/$date.log
3255 if [[ -e $f ]]; then
3256 files+=($f)
3257 fi
3258 done
3259 if (( ${#files[@]} >= 1 )); then
3260 cat ${files[@]} | tail
3261 hr
3262 fi
3263 done
3264 }
3265
3266 prof-sort() {
3267 case $HOSTNAME in
3268 kd)
3269 prof-recent-sort
3270 ;;
3271 *)
3272 ssh b8.nz prof-recent-sort
3273 ;;
3274 esac
3275 }
3276
3277 prof-recent-sort() {
3278 local d dates date files f
3279 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3280 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3281 files=()
3282 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3283 for date in ${dates[@]}; do
3284 f=$d/$date.log
3285 if [[ -e $f ]]; then
3286 files+=($f)
3287 fi
3288 done
3289 done
3290 for f in "${files[@]}"; do
3291 sed "s/\$/ $f/" $f
3292 done | sort
3293 }
3294
3295
3296 # usage: debvm DEBIAN_VERSION RAM_MB
3297 debvm() {
3298 local ver ram fname src
3299 ver=$1
3300 ram=${2:-2024}
3301 # * is because it might have -backports in the name. we only expect 1 expansion
3302 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3303 if (( ${#fnames[@]} >= 2 )); then
3304 echo "error: iank: unexpected multiple files"
3305 return 1
3306 fi
3307 fname="${fnames[0]}"
3308 src=/a/opt/roms/$fname
3309 if [[ ! -f $src ]]; then
3310 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3311 return 1
3312 fi
3313 cp -a $src /t
3314 # note, in fai-revm we do this: not sure why, maybe because of br device
3315 # --graphics spice,listen=0.0.0.0
3316 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3317 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3318
3319 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3320 # but happen to want to try out the debian cloud images. the upstream
3321 # requires python2 and hasn't really changed since the version in d10.
3322 #
3323 # apt install cvs2git cvs
3324 # # 7G was not enough
3325 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3326 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3327 ## www-rsync is an rsynced copy of the cvsfrom savannah
3328 }
3329
3330 mygajim() {
3331 local time time_sec time_pretty days
3332 days=${1:-16}
3333 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
3334 case $time in
3335 16*) : ;;
3336 *) continue ;;
3337 esac
3338 if ! time_pretty=$(date +%F.%R -d @$time); then
3339 echo bad time: $time
3340 return 1
3341 fi
3342 echo $time_pretty "$l"
3343 time_sec=${time%%.*}
3344 # only look at the last 18 days. generally just use this for timesheet.
3345 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3346 done
3347 }
3348
3349 allmygajim() {
3350 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3351 }
3352
3353 gajlogs() {
3354 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3355 }
3356
3357
3358 net-dev-info() {
3359 e "lspci -nnk|gr -iA2 net"
3360 lspci -nnk|gr -iA2 net
3361 hr
3362 e "s lshw -C network"
3363 hr
3364 sudo lshw -C network
3365 }
3366
3367 nk() {
3368 ser stop NetworkManager
3369 ser disable NetworkManager
3370 ser stop NetworkManager-wait-online.service
3371 ser disable NetworkManager-wait-online.service
3372 ser stop dnsmasq
3373 sudo resolvconf -d NetworkManager
3374 # ser start dnsmasq
3375 sudo ifup br0
3376 }
3377 ngo() {
3378 sudo ifdown br0
3379 ser start NetworkManager
3380 sleep 4
3381 sudo nmtui-connect
3382 }
3383
3384 otp() {
3385 oathtool --totp -b "$*" | xclip -selection clipboard
3386 }
3387 # run cmd and copy output
3388 j() {
3389 "$@" |& pee "xclip -r -selection clipboard" cat
3390 }
3391
3392 # xorg copy. copy text piped into command
3393 xc() {
3394 xclip -r -selection clipboard
3395 }
3396 # echo copy
3397 ec() {
3398 pee "xclip -r -selection clipboard" cat
3399 }
3400
3401 pakaraoke() {
3402 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3403 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
3404 }
3405
3406 pfind() { #find *$1* in $PATH
3407 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3408 local pathArray
3409 IFS=: pathArray=($PATH); unset IFS
3410 find "${pathArray[@]}" -iname "*$1*"
3411 }
3412
3413 pick-trash() {
3414 # trash-restore lists everything that has been trashed at or below CWD
3415 # This picks out files just in CWD, not subdirectories,
3416 # which also match grep $1, usually use $1 for a time string
3417 # which you get from running restore-trash once first
3418 local name x ask
3419 local nth=1
3420 # last condition is to not ask again for ones we skipped
3421 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3422 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3423 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3424 read -r -p "$name [Y/n] " ask
3425 if [[ ! $ask || $ask == [Yy] ]]; then
3426 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3427 echo $x | restore-trash > /dev/null
3428 elif [[ $ask == [Nn] ]]; then
3429 nth=$((nth+1))
3430 else
3431 return
3432 fi
3433 done
3434 }
3435
3436
3437 pub() {
3438 rld /a/h/_site/ li:/var/www/iankelling.org/html
3439 }
3440
3441
3442 pumpa() {
3443 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3444 # packages catches up on some changes in future (this is written in
3445 # 4/2017)
3446 #
3447 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3448 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3449 # work area; turns out it\'s impossible to set correctly if you are
3450 # not a fully EWMH compliant desktop environment
3451 #
3452 # geekosaur: chrome shows one failure mode, qt/kde another, other
3453 # gtk apps a third, ... I came up with a setting that works for me
3454 # locally but apparently doesnt work for others, so we joined the
3455 # other tiling window managers in giving up on setting it at all
3456 #
3457 xprop -root -remove _NET_WORKAREA
3458 command pumpa & r
3459 }
3460
3461 # reviewboard, used at my old job
3462 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3463 #rbp() { rbt post -o "$@"; }
3464
3465 rebr() {
3466 sudo ifdown br0
3467 sudo ifup br0
3468 }
3469
3470
3471 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3472 # only run on MAIL_HOST. simpler to keep this on one system.
3473 r2eadd() { # usage: name url
3474 # initial setup of rss2email:
3475 # r2e new r2e@iankelling.org
3476 # that initializes files, and sets default email.
3477 # symlink to the config doesnt work, so I copied it to /p/c
3478 # and then use cli option to specify explicit path.
3479 # Only option changed from default config is to set
3480 # force-from = True
3481 #
3482 # or else for a few feeds, the from address is set by the feed, and
3483 # if I fail delivery, then I send a bounce message to that from
3484 # address, which makes me be a spammer.
3485
3486 r2e add $1 "$2" $1@r2e.iankelling.org
3487 # get up to date and dont send old entries now:
3488 r2e run --no-send $1
3489 }
3490
3491 rspicy() { # usage: HOST DOMAIN
3492 # connect to spice vm remote host. use vspicy for local host
3493 local port
3494 # shellcheck disable=SC2087
3495 port=$(ssh $1<<EOF
3496 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3497 sed -rn "s/.*port='([0-9]+).*/\1/p"
3498 EOF
3499 )
3500 if [[ $port ]]; then
3501 spicy -h $1 -p $port
3502 else
3503 echo "error: no port found. check that the domain is running."
3504 fi
3505 }
3506
3507
3508 scssl() {
3509 # s gem install scss-lint
3510 pushd /a/opt/thoughtbot-guides
3511 git pull --stat
3512 popd
3513 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3514 }
3515
3516 skbrc() {
3517 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3518 }
3519
3520 skaraoke() {
3521 local tmp out
3522 out=${2:-${1%.*}.sh}
3523 tmp=$(mktemp -d)
3524 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3525 # todo, the current sleep seems pretty good, but it
3526 # would be nice to have an empirical measurement, or
3527 # some better wait to sync up.
3528 #
3529 # note: --loop-file=no prevents it from hanging if you have that
3530 # set to inf the mpv config.
3531 # --loop=no prevents it from exit code 3 due to stdin if you
3532 # had it set to inf in mpv config.
3533 #
3534 # args go to mpv, for example --volume=80, 50%
3535 cat >$out <<EOFOUTER
3536 #!/bin/bash
3537 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3538 ( sleep .2; scriptreplay <( cat <<'EOF'
3539 $(cat $tmp/timing)
3540 EOF
3541 ) <( cat <<'EOF'
3542 $(cat $tmp/typescript)
3543 EOF
3544 ))&
3545 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3546 $(base64 "$1")
3547 EOF
3548 kill 0
3549 EOFOUTER
3550 rm -r $tmp
3551 chmod +x $out
3552 }
3553
3554 smeld() { # ssh meld usage host1 host2 file
3555 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3556 }
3557
3558 spd() {
3559 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3560 }
3561
3562 spamf() { # spamtest on FILE
3563 if (( $# != 1 )); then
3564 e spamtest error: expected 1 arg, filename >&2
3565 return 1
3566 fi
3567 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3568 }
3569
3570
3571 # mail related
3572 testmail() {
3573 declare -gi _seq; _seq+=1
3574 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3575 # for testing to send from an external address, you can do for example
3576 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3577 # note in exim, you can retry a deferred message
3578 # s exim -M MSG_ID
3579 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3580 }
3581
3582 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3583 # make modifications, then copy to live file, use -eW to actually modify mailbox
3584 #
3585 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3586 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3587
3588 # sieve with output filter. arg is mailbox, like INBOX.
3589 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3590
3591 # always run this first, edit the test files, then run the following
3592 testsieve() {
3593 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
3594 }
3595 runsieve() {
3596 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3597 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3598 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
3599 }
3600
3601 # usage:
3602 # alertme SUBJECT
3603 # printf "subject\nbody\n" | alertme
3604 alertme() {
3605 if [[ -t 0 ]]; then
3606 exim -t <<EOF
3607 From: alertme@b8.nz
3608 To: alerts@iankelling.org
3609 Subject: $*
3610 EOF
3611 else
3612 read -r sub
3613 { cat <<EOF
3614 From: alertme@b8.nz
3615 To: alerts@iankelling.org
3616 Subject: $sub
3617
3618 EOF
3619 cat
3620 } | exim -t
3621 fi
3622 }
3623 daylertme() {
3624 if [[ -t 0 ]]; then
3625 exim -t <<EOF
3626 From: alertme@b8.nz
3627 To: daylert@iankelling.org
3628 Subject: $*
3629 EOF
3630 else
3631 read -r sub
3632 { cat <<EOF
3633 From: alertme@b8.nz
3634 To: daylert@iankelling.org
3635 Subject: $sub
3636
3637 EOF
3638 cat
3639 } | exim -t
3640 fi
3641 }
3642
3643 # alert when a page goes live.
3644 alert200() {
3645 local quiet url tmpdir
3646 quiet=false
3647 case $1 in
3648 # dont send a diff of the html. some html is not very readable
3649 -q) quiet=true
3650 shift
3651 ;;
3652 esac
3653 url="$1"
3654 tmpdir="$(mktemp -d)"
3655 cd $tmpdir
3656 while true; do
3657 if wget -q "$url"; then
3658 if $quiet; then
3659 echo | daylert 200
3660 else
3661 alertme $tmpdir
3662 fi
3663 fi
3664 sleep $(( 120 + RANDOM % 300 ))
3665 done
3666 }
3667
3668 # alert on changes to a webpage (just the base page that curl gets)
3669 # usage: weblert URL [SUBJECT...]
3670 weblert() {
3671 local u old new quiet
3672 quiet=false
3673 case $1 in
3674 # dont send a diff of the html. some html is not very readable
3675 -q) quiet=true
3676 shift
3677 ;;
3678 esac
3679 u="$1"
3680 shift
3681 subject="${*:-weblert}"
3682 old=$(curl -s "$u") ||:
3683 while true; do
3684 new=$(curl -s "$u") ||:
3685 if [[ $old && $new ]]; then
3686 if [[ $new != "$old" ]]; then
3687 if $quiet; then
3688 echo | daylertme "$subject"
3689 else
3690 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3691 fi
3692 fi
3693 old="$new"
3694 fi
3695 sleep $(( 60 + RANDOM % 120 ))
3696 done
3697 }
3698
3699 torshell() {
3700 # per man torsocks
3701 # shellcheck disable=SC1090 # expected
3702 source "$(type -p torsocks)" on
3703 }
3704
3705 eless2() {
3706 less /var/log/exim4/mymain
3707 }
3708
3709
3710 # mail related
3711 testexim() {
3712 # testmail above calls sendmail, which is a link to exim/postfix.
3713 # its docs dont say a way of adding an argument
3714 # to sendmail to turn on debug output. We could make a wrapper, but
3715 # that is a pain. Exim debug args are documented here:
3716 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3717 #
3718 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3719 # note, for exim daemon, you can turn on debug options by
3720 # adding -d, etc to COMMONOPTIONS in
3721 # /etc/default/exim4
3722 #
3723 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3724 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3725 #
3726 # -t = get recipient from header
3727 exim -d -t <<EOF
3728 From: ian@iankelling.org
3729 To: submit@b.b8.nz
3730 Subject: testbug1
3731
3732 Package: test
3733 Version:1
3734
3735 This is a test message.
3736 EOF
3737 }
3738
3739 # test bounce exim
3740 testbexim() {
3741 to=$1
3742 exim -d -f '<>' $to <<EOF
3743 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3744 To: $to
3745 Subject: Mail delivery failed: returning message to sender
3746
3747 This message was created automatically by mail delivery software.
3748 EOF
3749
3750 }
3751
3752
3753 # toggle keyboard
3754 tk() {
3755 # based on
3756 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3757 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3758 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3759 echo enabling keyboard
3760 # find the first slave keyboard number, they are all the same in my output.
3761 # if they werent, worst case we would need to save the slave number somewhere
3762 # when it got disabled.
3763 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3764 xinput reattach $id $slave
3765 else
3766 xinput float $id
3767 fi
3768 }
3769
3770 tm() {
3771 # timer in minutes
3772 # --no-config
3773 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
3774 }
3775
3776 ## usage: to connect to my main transmission daemon from a different host, run this
3777 trans-remote-route() {
3778 :
3779 }
3780 trg() { transmission-remote-gtk & r; }
3781 # TODO: this wont work transmission.lan doesnt exist
3782 trc() {
3783 # example, set global upload limit to 100 kilobytes:
3784 # trc -u 100
3785 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
3786 }
3787
3788 trysleep() {
3789 retries="$1"
3790 sleepsecs="$2"
3791 shift 2
3792 for (( i=0; i < retries - 1; i++ )); do
3793 if "$@"; then
3794 return 0
3795 fi
3796 sleep $sleepsecs
3797 done
3798 "$@"
3799 }
3800
3801
3802 tu() {
3803 local s
3804 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
3805 s=s;
3806 fi
3807 # full path for using in some initial setup steps
3808 $s /a/exe/teeu "$@"
3809 }
3810
3811 # execute exim in its namespace. Useful args like -Mrm
3812 enn() {
3813 local ecmd pid
3814
3815 ecmd="/usr/sbin/exim4 -C /etc/exim4/my.conf"
3816 if ip a show veth1-mail &>/dev/null; then
3817 s $ecmd "$@"
3818 else
3819 sdncmdroot exim4 $ecmd "$@"
3820 fi
3821 }
3822
3823 # get pid of systemd service
3824 servicepid() {
3825 local pid unit dir
3826 unit="$1"
3827 pid=$(systemctl show --property MainPID --value "$unit")
3828 case $pid in
3829 [1-9]*) : ;;
3830 *)
3831
3832 dir=/sys/fs/cgroup/system.slice
3833 if [[ ! -d $dir ]]; then
3834 # t10 and older directory.
3835 dir=/sys/fs/cgroup/systemd/system.slice
3836 fi
3837
3838 # 0 or empty. This file includes the MainPid, so I expect we
3839 # could just get this in the first place, but i don't know if that
3840 # is always the case.
3841 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
3842 ;;
3843 esac
3844 if [[ $pid ]]; then
3845 printf "%s\n" "$pid"
3846 else
3847 return 1
3848 fi
3849 }
3850
3851 sdnbash() { # systemd namespace bash
3852 local unit pid
3853 if (( $# != 1 )); then
3854 echo $0: error wrong number of args >&2
3855 return 1
3856 fi
3857 unit=$1
3858 pid=$(servicepid $unit)
3859 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
3860 }
3861
3862 sdnbashroot() { # systemd namespace bash as root
3863 local unit pid
3864 if (( $# != 1 )); then
3865 echo $0: error wrong number of args >&2
3866 return 1
3867 fi
3868 unit=$1
3869 pid=$(servicepid $unit)
3870 m sudo nsenter -t $pid -n -m bash
3871 }
3872
3873
3874 # systemd namespace cmd
3875 # usage: UNIT CMD...
3876 sdncmd() {
3877 local unit pid tmpf
3878 if (( $# <= 1 )); then
3879 echo $0: error wrong number of args >&2
3880 return 1
3881 fi
3882 unit=$1
3883 shift
3884 pid=$(servicepid $unit)
3885 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3886 export -p >$tmpf
3887 printf "%s " "${@@Q}" >>$tmpf
3888 echo >>$tmpf
3889 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
3890 }
3891
3892 sdncmdroot() { # systemd namespace root command
3893 local unit pid
3894 if (( $# < 2 )); then
3895 echo $0: error wrong number of args >&2
3896 return 1
3897 fi
3898 unit=$1
3899 shift
3900 pid=$(servicepid $unit)
3901 m sudo nsenter -t $pid -n -m "$@"
3902 }
3903
3904
3905 mailnnbash() {
3906 sdnbash mailnn
3907 }
3908
3909 # we use wireguard now, use mailnnbash.
3910 # mailvpnbash() {
3911 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
3912 # }
3913
3914 eximbash() {
3915 sdnbashroot exim4
3916 }
3917 spamnn() {
3918 local spamdpid
3919 spamdpid=$(systemctl show --property MainPID --value spamassassin)
3920 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
3921 }
3922 unboundbash() {
3923 sdnbashroot unbound
3924 }
3925
3926 nmtc() {
3927 s nmtui-connect "$@"
3928 }
3929
3930 mailnncheck() {
3931 local unit pid ns mailnn
3932 # mailvpn would belong on the list if using openvpn
3933 for unit in mailnn unbound dovecot spamassassin exim4 radicale; do
3934 pid=$(servicepid $unit)
3935 echo debug: unit=$unit pid=$pid
3936 if [[ ! $pid ]]; then
3937 echo failed to find pid for unit=$unit
3938 continue
3939 fi
3940 if ! ns=$(s readlink /proc/$pid/ns/net); then
3941 echo failed to find ns for unit=$unit pid=$pid
3942 continue
3943 fi
3944 if [[ $mailnn ]]; then
3945 if [[ $ns != "$mailnn" ]]; then
3946 echo "$unit ns $ns != $mailnn"
3947 fi
3948 else
3949 mailnn=$ns
3950 fi
3951 done
3952
3953 }
3954
3955
3956 vpncmd() {
3957 sdncmd openvpn-client-tr@client.service "$@"
3958 }
3959 vpni() {
3960 sdncmd openvpn-client-tr@client.service bash
3961 }
3962 vpnbash() {
3963 sdncmdroot openvpn-client-tr@client.service bash
3964 }
3965
3966
3967 vpn() {
3968 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
3969 local vpn_service=openvpn-client
3970 else
3971 local vpn_service=openvpn
3972 fi
3973
3974 [[ $1 ]] || { echo need arg; return 1; }
3975 journalctl --unit=$vpn_service@$1 -f -n0 &
3976 # sometimes the journal doesnt open until after the vpn output
3977 # has happened. hoping this fixes that.
3978 sleep 1
3979 sudo systemctl start $vpn_service@$1
3980 # sometimes the ask-password agent does not work and needs a delay.
3981 sleep .5
3982 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
3983 # noticed around 8-2017 after update from around stretch release
3984 # on debian testing, even though the bug is much older.
3985 sudo systemd-tty-ask-password-agent
3986 }
3987
3988 fixu() {
3989 local stats
3990 ls -lad /run/user/1000
3991 stats=$(stat -c%a-%g-%u /run/user/1000)
3992 if [[ $stats != 700-1000-1000 ]]; then
3993 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
3994 fi
3995 }
3996
3997 # unmute
3998 um() {
3999 local sink card
4000 sink=$(pactl get-default-sink)
4001 if [[ $sink == auto_null ]]; then
4002 # guessing there is just one with an off profile. otherwise we will
4003 # need some other solution, like storing the card identifier that we
4004 # muted with nap.
4005 card=$(pacmd list-cards | sed -n '/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}')
4006 m pacmd set-card-profile "$card" output:analog-stereo
4007 fi
4008
4009 m pactl set-sink-mute @DEFAULT_SINK@ false
4010 rm -f /tmp/ianknap
4011 }
4012
4013 nap() {
4014 local sink card
4015 sink=$(pactl get-default-sink)
4016 card="${sink%.*}"
4017 card="${card/output/card}"
4018 m pacmd set-card-profile "$card" off
4019
4020 # clicking on a link in a browser can cause unmute.
4021 # I don't want that. So, use a stronger form of mute
4022 # than this.
4023 #pactl set-sink-mute @DEFAULT_SINK@ true
4024 touch /tmp/ianknap
4025 }
4026
4027
4028 # systemctl is-enabled / status / cat says nothing, instead theres
4029 # some obscure symlink. paths copied from man systemd.unit.
4030 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
4031 # seru list-dependencies --reverse --all UNIT
4032 sysd-deps() {
4033 local f
4034 local -a dirs search
4035 ngset
4036
4037 case $1 in
4038 u)
4039 search=(
4040 ~/.config/systemd/user.control/*
4041 $XDG_RUNTIME_DIR/systemd/user.control/*
4042 $XDG_RUNTIME_DIR/systemd/transient/*
4043 $XDG_RUNTIME_DIR/systemd/generator.early/*
4044 ~/.config/systemd/user/*
4045 /etc/systemd/user/*
4046 $XDG_RUNTIME_DIR/systemd/user/*
4047 /run/systemd/user/*
4048 $XDG_RUNTIME_DIR/systemd/generator/*
4049 ~/.local/share/systemd/user/*
4050 /usr/lib/systemd/user/*
4051 $XDG_RUNTIME_DIR/systemd/generator.late/*
4052 )
4053 ;;
4054 *)
4055 search=(
4056 /etc/systemd/system.control/*
4057 /run/systemd/system.control/*
4058 /run/systemd/transient/*
4059 /run/systemd/generator.early/*
4060 /etc/systemd/system/*
4061 /etc/systemd/systemd.attached/*
4062 /run/systemd/system/*
4063 /run/systemd/systemd.attached/*
4064 /run/systemd/generator/*
4065 /lib/systemd/system/*
4066 /run/systemd/generator.late/*
4067 )
4068 ;;
4069 esac
4070 for f in "${search[@]}"; do
4071 [[ -d $f ]] || continue
4072 case $f in
4073 *.requires|*.wants)
4074 dirs+=("$f")
4075 ;;
4076 esac
4077 done
4078 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4079 case ${#dirs[@]} in
4080 1)
4081 echo "${dirs[0]}:"
4082 ll "${dirs[@]}"
4083 ;;
4084 0) : ;;
4085 *)
4086 ll "${dirs[@]}"
4087 ;;
4088 esac
4089 ngreset
4090 }
4091
4092 fixvpndns() {
4093 local link istls
4094 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4095 case $istls in
4096 yes|no) : ;;
4097 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4098 esac
4099 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4100 }
4101
4102 vpnoff() {
4103 [[ $1 ]] || { echo need arg; return 1; }
4104 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4105 local vpn_service=openvpn-client
4106 else
4107 local vpn_service=openvpn
4108 fi
4109 sudo systemctl stop $vpn_service@$1
4110 }
4111 vpnoffc() { # vpn off client
4112 ser stop openvpn-client-tr@client
4113 }
4114 vpnc() {
4115 local unit
4116 unit=openvpn-client-tr@client
4117 sudo -v
4118 if [[ $(systemctl is-active $unit) != active ]]; then
4119 s systemctl start $unit
4120 sleep 1
4121 fi
4122 }
4123
4124
4125 vspicy() { # usage: VIRSH_DOMAIN
4126 # connect to vms made with virt-install
4127 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4128 sed -r "s/.*port='([0-9]+).*/\1/")"
4129 }
4130
4131 wian() {
4132 cat-new-files /m/4e/INBOX/new
4133 }
4134 wakehours() {
4135 local sec
4136 if (( $# != 1 )) ; then
4137 echo wakehours: error: expected 1 arg, got $# >&2
4138 return 1
4139 fi
4140 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4141 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4142 }
4143
4144 calvis() { # calendar visualize
4145 install -m 600 /dev/null /tmp/calendar-bytes
4146 while read -r l; do
4147 for char in $l; do
4148 # shellcheck disable=SC2059 # intentional for the hex formatting
4149 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4150 done
4151 done < <(grep -v '[#-]' /p/calendar-data)
4152 /p/c/proc/calendar/linux-amd64/calendar
4153 }
4154
4155 wtr() { curl wttr.in/boston; }
4156
4157 xevkb() { xev -event keyboard; }
4158
4159 # * misc stuff
4160
4161 vrun() {
4162 printf "running: %s\n" "$*"
4163 "$@"
4164 }
4165
4166 electrum() {
4167 # https://electrum.readthedocs.io/en/latest/tor.html
4168 # https://github.com/spesmilo/electrum-docs/issues/129
4169 s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4170 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4171 }
4172 monero() {
4173 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4174 }
4175
4176
4177 # rg my main files
4178 rgm() {
4179 rg "$@" /p/w.org /a/t.org /a/work.org /b
4180 }
4181
4182 # re all my files more expansively
4183
4184 rem() {
4185 local paths
4186 paths="/p/c /b/"
4187 find $paths -not \( -name .svn -prune -o -name .git -prune \
4188 -o -name .hg -prune -o -name .editor-backups -prune \
4189 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4190 rgv $local_rgv_args -g "!bash_unpublished" -- "$*" $paths /a/work.org ||:
4191 }
4192 reml() { # rem with limit to 5 matches per file
4193 local_rgv_args="-m 5"
4194 rem "$@"
4195 }
4196
4197 rep() {
4198 local paths
4199 paths="/p/c"
4200 find $paths -not \( -name .svn -prune -o -name .git -prune \
4201 -o -name .hg -prune -o -name .editor-backups -prune \
4202 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4203 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4204 }
4205 repl() { # rem with limit to 5 matches per file
4206 local local_rgv_args="-m 5"
4207 rem "$@"
4208 }
4209
4210
4211 # re on common fsf files
4212 ref() {
4213 local paths
4214 paths="/f/gluestick /f/brains /f/s /c"
4215 find $paths -not \( -name .svn -prune -o -name .git -prune \
4216 -o -name .hg -prune -o -name .editor-backups -prune \
4217 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4218 rgv -- "$*" $paths /a/work.org ||:
4219 }
4220
4221
4222 # for use in /f/bind
4223 fupzone() {
4224 # shellcheck disable=SC2046 # i want word splitting
4225 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4226 }
4227
4228 # setup:
4229 # pip3 install linode-cli
4230 # linode-cli
4231 livp9() {
4232 local input ip id tmp
4233 input=$1
4234 if [[ $2 ]]; then
4235 id=$2
4236 ip=$3
4237 else
4238 tmp=$(mktemp)
4239 echo $tmp
4240 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4241 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4242 for string in $ip $id; do
4243 case $string in
4244 [0-9]*) : ;;
4245 *)
4246 echo "livp9: bad value ip=$ip id=$id input=$input"
4247 return 1
4248 ;;
4249 esac
4250 done
4251 rm $tmp
4252
4253 while true; do
4254 if timeout 4 ssh $ip :; then
4255 break
4256 fi
4257 sleep 3
4258 done
4259 fi
4260 ssh $ip <<EOF
4261 apt-get -qq update
4262 apt-get -qq -y install ffmpeg rsync
4263 mkdir vp9
4264 EOF
4265 m rsync $input $ip:
4266 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
4267 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
4268 rsync $ip:vp9/$input vp9
4269 linode-cli linodes delete $id
4270 }
4271
4272 reset-konsole() {
4273 # we also have a file in /a/c/...konsole...
4274 local f=$HOME/.config/konsolerc
4275 setini DefaultProfile profileian.profile "Desktop Entry" $f
4276 setini Favorites profileian.profile "Favorite Profiles" $f
4277 setini ShowMenuBarByDefault false KonsoleWindow $f
4278 setini TabBarPosition Top TabBar $f
4279 }
4280
4281 reset-sakura() {
4282 while read -r k v; do
4283 # shellcheck disable=SC2154
4284 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4285 done <<'EOF'
4286 colorset1_back rgb(33,37,39)
4287 less_questions true
4288 audible_bell No
4289 visible_bell No
4290 disable_numbered_tabswitch true
4291 scroll_lines 10000000
4292 scrollbar true
4293 EOF
4294 }
4295
4296 # make a page of links found in the files $@. redirect output
4297 linkhtml() {
4298 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4299 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4300 }
4301
4302 reset-xscreensaver() {
4303 # except for spash, i set these by setting gui options in
4304 # xscreensaver-command -demo
4305 # then finding the corresponding option in .xscreensaver
4306 # spash, i happened to notice in .xscreensaver
4307 #
4308 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4309 cat > /home/iank/.xscreensaver <<'EOF'
4310 mode: blank
4311 dpmsEnabled: True
4312 dpmsStandby: 0:07:00
4313 dpmsSuspend: 0:08:00
4314 dpmsOff: 0:00:00
4315 timeout: 0:05:00
4316 lock: True
4317 lockTimeout: 0:06:00
4318 splash: False
4319 EOF
4320
4321 }
4322
4323
4324 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4325 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4326
4327
4328 # * stuff that makes sense to be at the end
4329 if [[ "$SUDOD" ]]; then
4330 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4331 cd "$SUDOD" ||:
4332 unset SUDOD
4333 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4334 cd /a
4335 OLDPWD=
4336 fi
4337
4338
4339
4340
4341 # for mitmproxy to get a newer python.
4342 # commented until i want to use it because it
4343 # noticably slows bash startup
4344 #
4345
4346 mypyenvinit () {
4347 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4348 echo "error: dont be root. make sure pyenv is installed"
4349 return 1
4350 fi
4351 export PATH="$HOME/.pyenv/bin:$PATH"
4352 eval "$(pyenv init -)"
4353 eval "$(pyenv virtualenv-init -)"
4354 }
4355
4356
4357
4358 # I have the git repo and a release. either one should work.
4359 # I have both because I was trying to solve an issue that
4360 # turned out to be unrelated.
4361 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4362
4363 ## i should have documented this...
4364 # based on https://github.com/keyboardio/Kaleidoscope
4365 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4366
4367 # They want to be added to the start, but i think
4368 # that should be avoided unless we really need it.
4369 path-add --end ~/.npm-global
4370
4371
4372 path-add --end $HOME/.cargo/bin
4373
4374 if type -P rg &>/dev/null; then
4375 # --no-messages because of annoying errors on broken symlinks
4376 # -z = search .gz etc files
4377 # -. = search dotfiles
4378 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 $?; }
4379 #fails if not exist. ignore
4380 complete -r rg 2>/dev/null ||:
4381 else
4382 alias rg=grr
4383 fi
4384
4385 # rg with respecting vcs ignore files
4386 rgv() {
4387 ret=0
4388 # settings that are turned off for pipes, keep them on.
4389 # Found by searching for "terminal" in --help
4390 # --heading
4391 # -n
4392 #
4393 # -. = search dotfiles
4394 # -z = search zipped files
4395 # -i = case insensitive
4396 # -M = max columns
4397 # --no-messages because of annoying errors on broken symlinks
4398 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4399 command rg -n --heading -. -z --no-messages -i -M 900 --no-ignore-parent -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" || ret=$?
4400 return $ret
4401 }
4402
4403 amall() {
4404 echo "$(tput setaf 5 2>/dev/null ||:)█ coresite █$(tput sgr0 2>/dev/null||:)"
4405 amfsf "$@"
4406 echo "$(tput setaf 5 2>/dev/null ||:)█ office █$(tput sgr0 2>/dev/null||:)"
4407 amoffice "$@"
4408 }
4409 amallq() { # amall quiet
4410 amfsf "$@"
4411 amoffice "$@"
4412 }
4413 amfsf() {
4414 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4415 amtool "$@"
4416 }
4417 amoffice() {
4418 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4419 amtool "$@"
4420 }
4421 amls() {
4422 amall silence query "$@"
4423 }
4424 # amtool silence add
4425 amsa() {
4426 amall silence add "$@"
4427 }
4428 # amtool silence force
4429 amsf() {
4430 amall silence add x!="1"
4431 }
4432 amrmall() {
4433 # note: not sure if quoting of this arg is correct
4434 amfsf silence expire "$(amfsf silence query -q)"
4435 amoffice silence expire "$(amoffice silence query -q)"
4436 }
4437
4438
4439 youtube-dl-update() {
4440 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4441 sudo chmod a+rx /usr/local/bin/youtube-dl
4442 }
4443
4444 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4445 yt-dlp-update() {
4446 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4447 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4448 }
4449
4450 mpvyt() {
4451 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4452 }
4453
4454 # taken from default changes to bashrc and bash_profile
4455 path-add --end --ifexists $HOME/.rvm/bin
4456 # also had ruby bin dir, but moved that to environment.sh
4457 # so its included in overall env
4458
4459
4460 # ya, hacky hardcoded hostnames in 2023. we could do better
4461 hssh-update() {
4462 local -a failed_hosts hosts
4463 case $HOSTNAME in
4464 sy|kd)
4465 hosts=(
4466 kd x3.office.fsf.org syw
4467 )
4468 ;;
4469 x3)
4470 hosts=(
4471 b8.nz sywg.b8.nz
4472 )
4473 ;;
4474 esac
4475 for host in ${hosts[@]}; do
4476 e $host
4477 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4478 failed_hosts+=($host)
4479 fi
4480 done
4481 if (( ${#failed_hosts[@]} >= 1 )); then
4482 echo failed_hosts=${failed_hosts[*]}
4483 return 1
4484 fi
4485 }
4486
4487 noi3bar() {
4488 touch /tmp/noi3bar
4489 }
4490 i3bar() {
4491 rm -fv /tmp/noi3bar
4492 }
4493
4494 # example:
4495 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4496 #
4497 attach-txt() {
4498 local f
4499 for f; do
4500 if [[ ! -s $f ]]; then
4501 e "error: empty or non-existent file $f"
4502 return 1
4503 fi
4504 done
4505 for f; do
4506 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4507 done | ec
4508 }
4509
4510 ctof() {
4511 units "tempC($1)" tempF
4512 }
4513
4514 ftoc() {
4515 units "tempF($1)" tempC
4516 }
4517
4518 # requires dns/firewall setup first
4519 local-icecast() {
4520 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4521 <Location "/fsf.webm">
4522 AuthType Basic
4523 AuthName "basic_auth"
4524 # created with
4525 # htpasswd -c icecast-fsf-htpasswd USERNAME
4526 AuthUserFile "/etc/icecast-fsf-htpasswd"
4527 Require valid-user
4528 </Location>
4529 <Location "/fsf-tech.webm">
4530 AuthType Basic
4531 AuthName "basic_auth"
4532 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4533 Require valid-user
4534 </Location>
4535 EOF
4536 }
4537
4538 # obs screen switching of
4539 obof() {
4540 ls -l /tmp/no-obs-auto-scene-switch
4541 touch /tmp/no-obs-auto-scene-switch
4542 }
4543 # obs screen switching on
4544 obon() {
4545 ls -l /tmp/no-obs-auto-scene-switch
4546 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4547 rm -f /tmp/no-obs-auto-scene-switch
4548 fi
4549 }
4550
4551 obs-gen-profiles() {
4552 local p=/p/c/basic/profiles
4553 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4554 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4555 }
4556
4557 # terminal clear. like clear, but put the prompt at the bottom,
4558 # useful for obs streaming the bottom half of a terminal window.
4559 tclear() {
4560 for ((i=0; i<COLUMNS; i++)); do
4561 echo
4562 done
4563 }
4564
4565
4566 export BASEFILE_DIR=/a/bin/fai-basefiles
4567
4568 #export ANDROID_HOME=/a/opt/android-home
4569 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
4570 #export USE_SDK_WRAPPER=yes
4571 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
4572
4573 # didnt get drush working, if I did, this seems like the
4574 # only good thing to include for it.
4575 # Include Drush completion.
4576 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
4577 # source /home/ian/.drush/drush.complete.sh
4578 # fi
4579
4580
4581 # best practice
4582 unset IFS
4583
4584 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
4585 # i added an extra condition as gentoo xorg guide says depending on
4586 # $DISPLAY is fragile.
4587 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
4588 exec startx
4589 fi
4590
4591
4592 # ensure no bad programs appending to this file will have an affect
4593 return 0