host info updates
[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 # note: the b8.nz suffix is for the User part, the IdentityFile
2656 # part is redundant to *.b8.nz.
2657 root_hosts+=($host i$host $host.b8.nz i$host.b8.nz)
2658 else
2659 nonroot_hosts+=($host i$host)
2660 fi
2661
2662 host_ips[$host]=$ip
2663 host_macs[$host]=$mac
2664 done </p/c/host-info
2665
2666 cedit /p/c/subdir_files/.ssh/config <<EOF || [[ $? == 1 ]]
2667 Host ${nonroot_hosts[@]}
2668 User iank
2669 IdentityFile ~/.ssh/home
2670
2671 Host ${root_hosts[@]}
2672 IdentityFile ~/.ssh/home
2673 EOF
2674
2675
2676 grep -E '^[a-z0-9]+[[:space:]]' /p/c/machine_specific/vps/bind-initial/db.b8.nz | awk '{print $1,$3}'
2677
2678 local host ipsuf f files
2679
2680 sedi '/edits below here are made automatically/,$d' /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf
2681 for host in ${!vpn_ips[@]}; do
2682 ipsuf=${vpn_ips[$host]}
2683 wghole $host $ipsuf
2684 u /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service <<EOF
2685 [Unit]
2686 Description=OpenVPN tunnel for %I
2687 After=syslog.target network-online.target
2688 Wants=network-online.target
2689 Documentation=man:openvpn(8)
2690 Documentation=https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
2691 Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
2692 Requires=iptables.service
2693
2694 [Service]
2695 Type=notify
2696 RuntimeDirectory=openvpn-client
2697 RuntimeDirectoryMode=0710
2698 WorkingDirectory=/etc/openvpn/client
2699 ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config /etc/openvpn/client/%i.conf
2700 # todo, try reenabling this from the default openvpn,
2701 # it was disabled so we could do bind mounts as a command,
2702 # but now systemd handles it
2703 #CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE
2704 LimitNPROC=10
2705 # DeviceAllow=/dev/null rw
2706 # DeviceAllow=/dev/net/tun rw
2707
2708 # we use .1 to make this be on a different network than kd, so that we can
2709 # talk to transmission on kd from remote host, and still use this
2710 # vpn.
2711 ExecStartPre=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns -n 10.174.$ipsuf start %i
2712 ExecStartPre=/sbin/iptables-restore /a/bin/distro-setup/transmission-firewall/netns.rules
2713 # allow wireguard network to connect
2714 ExecStartPre=/usr/sbin/ip r add 10.8.0.0/24 via 10.174.$ipsuf.1 dev veth1-client
2715 ExecStopPost=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns stop %i
2716 PrivateNetwork=true
2717 BindReadOnlyPaths=/etc/tr-resolv:/run/systemd/resolve:norbind /etc/basic-nsswitch:/etc/resolved-nsswitch:norbind
2718
2719 [Install]
2720 WantedBy=multi-user.target
2721 EOF
2722 done
2723
2724 {
2725 echo "cat <<EOF"
2726 for host in ${!host_ips[@]}; do
2727 ipsuf=${host_ips[$host]}
2728 echo 'local-data-ptr: "$l.'$ipsuf $host.b8.nz'"'
2729 done
2730 echo "EOF"
2731 } | u /p/ptr-data
2732
2733 {
2734 echo "cat <<EOF"
2735 for host in ${!host_ips[@]}; do
2736 ipsuf=${host_ips[$host]}
2737 echo "dhcp-host=${host_macs[$host]},set:$host,\$l.$ipsuf,$host"
2738 done
2739 echo "EOF"
2740 } | u /p/dnsmasq-data
2741
2742 {
2743 for host in ${!nonvpn_ips[@]}; do
2744 ipsuf=${nonvpn_ips[$host]}
2745 echo "$host A 10.2.0.$ipsuf"
2746 done
2747 for host in ${!vpn_ips[@]}; do
2748 ipsuf=${vpn_ips[$host]}
2749 cat <<EOF
2750 $host A 10.2.0.$ipsuf
2751 ${host}wg A 10.8.0.$ipsuf
2752 ${host}vp A 10.5.5.$ipsuf
2753 ${host}tr A 10.174.$ipsuf.2
2754 EOF
2755 done
2756 } | cedit vpn-ips-update /p/c/machine_specific/vps/bind-initial/db.b8.nz ||:
2757
2758
2759 echo checking for stray files:
2760
2761 initial_dir=$PWD
2762 cd /a/bin/ds/machine_specific
2763 ngset
2764 files=( */filesystem/etc/systemd/system/openvpn-client-tr@.service )
2765 ngreset
2766 cd $initial_dir
2767 for f in "${files[@]}"; do
2768 host=${f%%/*}
2769 if [[ ! ${vpn_ips[$host]} ]]; then
2770 e /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service
2771 fi
2772 done
2773
2774 cd /p/c/machine_specific
2775 ngset
2776 files=( */filesystem/etc/wireguard/wghole.conf )
2777 ngreset
2778 cd $initial_dir
2779 for f in "${files[@]}"; do
2780 host=${f%%/*}
2781 if [[ ! ${vpn_ips[$host]} ]]; then
2782 e rm /p/c/machine_specific/$host/filesystem/etc/wireguard/wghole.conf
2783 fi
2784 done
2785 }
2786
2787 # usage host ipsuf [extrahost]
2788 #
2789 # If the keys already exist and you want new ones, remove them:
2790 # rm /p/c/machine_specific/$host/filesystem/etc/wireguard/hole-{priv,pub}.key
2791 #
2792 # extrahost is a host/cidr that is allowed to go be routed through the
2793 # vpn by this host.
2794 wghole() {
2795 if (( $# < 2 || $# > 3 )); then
2796 e expected 2-3 arg of hostname, ip suffix, and extrahost >&2
2797 return 1
2798 fi
2799 local host ipsuf umask_orig vpn_allowed
2800 host=$1
2801 ipsuf=$2
2802 if [[ $3 ]]; then
2803 extrahost=,$3
2804 fi
2805 for vpn_host in ${!vpn_ips[@]}; do
2806 if [[ $vpn_host == "$host" ]]; then
2807 continue
2808 fi
2809 vpn_allowed+=",10.174.${vpn_ips[$vpn_host]}.2/32"
2810 done
2811 mkdir -p /p/c/machine_specific/$host/filesystem/etc/wireguard
2812 (
2813 cd /p/c/machine_specific/$host/filesystem/etc/wireguard
2814 umask_orig=$(umask)
2815 umask 0077
2816 if [[ ! -s hole-priv.key || ! -s hole-pub.key ]]; then
2817 wg genkey | tee hole-priv.key | wg pubkey > hole-pub.key
2818 fi
2819 cat >wghole.conf <<EOF
2820 [Interface]
2821 # contents hole-priv.key
2822 PrivateKey = $(cat hole-priv.key)
2823 ListenPort = 1194
2824 Address = 10.8.0.$ipsuf/24
2825 # https://dev.to/tangramvision/what-they-don-t-tell-you-about-setting-up-a-wireguard-vpn-1h2g
2826 # ||: makes the systemd service not fail due to the failed command
2827 PostUp = ping -w10 -c1 10.8.0.1 ||:
2828
2829 [Peer]
2830 # li. called wgmail on that server
2831 PublicKey = CTFsje45qLAU44AbX71Vo+xFJ6rt7Cu6+vdMGyWjBjU=
2832 AllowedIPs = 10.8.0.0/24$vpn_allowed$extrahost
2833 Endpoint = 72.14.176.105:1194
2834 PersistentKeepalive = 25
2835 EOF
2836 umask $umask_orig
2837 # old approach. systemd seems to work fine and cleaner.
2838 rm -f ../network/interfaces.d/wghole
2839 cedit -q $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf <<EOF || [[ $? == 1 ]]
2840 [Peer]
2841 PublicKey = $(cat hole-pub.key)
2842 AllowedIPs = 10.8.0.$ipsuf/32,10.174.${vpn_ips[$host]}.2/32
2843 EOF
2844 )
2845 }
2846
2847
2848 mns() { # mount namespace
2849 ns=$1
2850 shift
2851 s mkdir -p /root/mount_namespaces
2852 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2853 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2854 fi
2855 m sudo mount --make-private /root/mount_namespaces
2856 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2857 m sudo touch /root/mount_namespaces/$ns
2858 fi
2859 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2860 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2861 fi
2862 m sudo -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
2863 }
2864
2865 mnsd() { # mount namespace + systemd namespace
2866 ns=$1
2867 unit=$2
2868 shift 2
2869
2870 s mkdir -p /root/mount_namespaces
2871 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2872 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2873 fi
2874 m sudo mount --make-private /root/mount_namespaces
2875 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2876 m sudo touch /root/mount_namespaces/$ns
2877 fi
2878 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2879 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2880 fi
2881
2882 pid=$(servicepid $unit)
2883 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
2884 export -p >$tmpf
2885 printf "%s " "${@@Q}" >>$tmpf
2886 echo >>$tmpf
2887
2888 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
2889 }
2890
2891
2892 mnsr() { # mns run
2893 local ns=$1
2894 shift
2895 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
2896 }
2897
2898 mnsnonetr() {
2899 ns=$1
2900 lomh
2901 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2902 s ip netns add nonet
2903 fi
2904 mns $ns --net=/var/run/netns/nonet /bin/bash
2905 lomh
2906 }
2907
2908 mnsnonet() {
2909 ns=$1
2910 lomh
2911 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2912 s ip netns add nonet
2913 fi
2914 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
2915 lomh
2916 }
2917
2918
2919 lom() {
2920 # l = the loopback device
2921 local l base
2922 # get sudo pass cached right away
2923 if ! sudo -nv 2>/dev/null; then
2924 sudo -v
2925 fi
2926 if [[ $1 == /* ]]; then
2927 base=${1##*/}
2928 fs_file=$1
2929 if mns $base mountpoint -q /mnt/$base; then
2930 return 0
2931 fi
2932 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
2933 if [[ ! $l ]]; then
2934 l=$(sudo losetup -f)
2935 m sudo losetup $l $fs_file
2936 fi
2937 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2938 if ! m sudo cryptsetup luksOpen $l $base; then
2939 m sudo losetup -d $l
2940 return 1
2941 fi
2942 fi
2943 m sudo mkdir -p /mnt/$base
2944 m mns $base mount /dev/mapper/$base /mnt/$base
2945 m mns $base chown $USER:$USER /mnt/$base
2946 lomh
2947 else
2948 base=$1
2949 if mns $base mountpoint /mnt/$base &>/dev/null; then
2950 m mns $base umount /mnt/$base
2951 fi
2952 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2953 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
2954 echo lom: failed cryptsetup luksClose /dev/mapper/$base
2955 return 1
2956 fi
2957 fi
2958 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
2959 if [[ $l ]]; then
2960 m sudo losetup -d $l
2961 else
2962 echo lom: warning: no loopback device found
2963 fi
2964 fi
2965 }
2966
2967 # mu personality. for original, just run mp. for 2, run mp 2.
2968 # this is partly duplicated in mail-setup
2969 mp() {
2970 local dead=false
2971 for s in {1..5}; do
2972 if ! killall mu; then
2973 dead=true
2974 break
2975 fi
2976 sleep 1
2977 done
2978 if ! $dead; then
2979 echo error: mu not dead
2980 m psg mu
2981 return 1
2982 fi
2983 suf=$1
2984 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
2985 while (($#)); do
2986 target=$1$suf
2987 f=$2
2988 shift 2
2989 if [[ -e $f && ! -L $f ]]; then
2990 m rm -rf $f
2991 fi
2992 m ln -sf -T $target $f
2993 done
2994 }
2995
2996 # maildir enable
2997 mdenable() {
2998 local md dst ln_path src two
2999
3000 two=false
3001 case $1 in
3002 -2) two=true; shift ;;
3003 esac
3004
3005 for md; do
3006 src=
3007 if $two; then
3008 dst=/m/4e2/$md
3009 else
3010 dst=/m/4e/$md
3011 fi
3012
3013 ln_path=/m/md/$md
3014 for d in /m/md/$md /m/4e2/$md; do
3015 if [[ -d $d && ! -L $d ]]; then
3016 src=$d
3017 break
3018 fi
3019 done
3020 if [[ ! $src ]]; then
3021 echo "error: could not find $md" >&2
3022 return 1
3023 fi
3024 m mv -T $src $dst
3025 m ln -sf -T $dst $ln_path
3026 done
3027 }
3028 md2enable() {
3029 mdenable -2 "$@"
3030 }
3031 mddisable() {
3032 local md=$1
3033 dst=/m/md/$md
3034
3035 ### begin copied from mdenable, but different d ###
3036 for d in /m/4e/$md /m/4e2/$md; do
3037 if [[ -d $d && ! -L $d ]]; then
3038 src=$d
3039 break
3040 fi
3041 done
3042 if [[ ! $src ]]; then
3043 echo "error: could not find $md" >&2
3044 return 1
3045 fi
3046 ### end copy from mdenable ###
3047
3048 if [[ -L $dst ]]; then m rm $dst; fi
3049 m mv -T $src $dst
3050 }
3051
3052
3053 mdt() {
3054 markdown "$1" >/tmp/mdtest.html
3055 firefox /tmp/mdtest.html
3056 }
3057
3058 mo() { xset dpms force off; } # monitor off
3059
3060 mpvgpu() {
3061 # seems to be the best gpu decoding on my nvidia 670.
3062 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3063
3064
3065 case $HOSTNAME in
3066 kd)
3067 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3068 ;;
3069 esac
3070 # going back to the default slow clock, and slower fan:
3071 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3072 if [[ $DISPLAY ]]; then
3073 mpv --vo=vdpau --hwdec=auto "$@"
3074 else
3075 # waylandvk seems to work the same
3076 mpv --gpu-context=wayland --hwdec=auto
3077 fi
3078 }
3079
3080 mpvd() {
3081 mpv --profile=d "$@";
3082 }
3083 mpva() {
3084 mpv --profile=a "$@";
3085 }
3086 # mpv all media files in . or $1
3087 mpvm() {
3088 local -a extensions arg
3089 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3090 # into /a/x.log, then
3091 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3092
3093 # note: to join them together for a regex, do:
3094 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3095 extensions=(
3096 .webm
3097 .mkv
3098 .flv
3099 .flv
3100 .vob
3101 .ogv .ogg
3102 .drc
3103 .gif
3104 .gifv
3105 .mng
3106 .avi
3107 .MTS .M2TS .TS
3108 .mov .qt
3109 .wmv
3110 .yuv
3111 .rm
3112 .rmvb
3113 .viv
3114 .asf
3115 .amv
3116 .mp4 .m4p .m4v
3117 .mpg .mp2 .mpeg .mpe .mpv
3118 .mpg .mpeg .m2v
3119 .m4v
3120 .svi
3121 .3gp
3122 .3g2
3123 .mxf
3124 .roq
3125 .nsv
3126 )
3127 arg=("(" -iname "*${extensions[0]}")
3128 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3129 arg+=(-o -iname "*${extensions[i]}")
3130 done
3131 arg+=(")")
3132 dir=${1:-.}
3133 # debug:
3134 #find $dir "${arg[@]}" -size +200k
3135 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3136 }
3137 mpvs() {
3138 mpv --profile=s "$@";
3139 }
3140
3141 myirc() {
3142 if [[ ! $1 ]]; then
3143 set -- fsfsys
3144 fi
3145 local -a d
3146 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3147 # use * instead of -r since that does sorted order
3148 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3149 }
3150
3151
3152 allmyirc() {
3153 local d
3154 d=/var/lib/znc/moddata/log/iank/freenode
3155 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3156 }
3157
3158 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3159 # is that there are sometimes duplicates, and sometimes the a log file
3160 # is for a specific day yet logs messages for subsequent days, and the
3161 # only way to realize that is to notice that the timestamps rolled over
3162 # into a new day, you can't see it in isolation. So, basically, pidgin
3163 # logs are really annoying to read a grep of my messages to find the
3164 # date and time I said when I started and stopped working, so I'm trying
3165 # out a new client: profanity.
3166 mypidgin() {
3167 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3168 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3169 # shellcheck disable=SC2016 # false positive on ${
3170 grep -A1 ') iank:' ./*.txt \
3171 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3172 s/^[^ ]*\.txt-//
3173 /^--$/d
3174 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3175 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3176 }
3177
3178 # my profanity
3179 #
3180 myprof() {
3181 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3182 logs=(*)
3183 logcount=${#logs[@]}
3184 if (( logcount > 15 )); then
3185 i=$(( logcount - 15 ))
3186 else
3187 i=0
3188 fi
3189 # usually do this on monday, sometimes later
3190 if [[ $(date +%A) == Monday ]]; then
3191 min_date=$(date -d 'monday 2 weeks ago' +%s)
3192 else
3193 min_date=$(date -d 'monday 3 weeks ago' +%s)
3194 fi
3195 for (( ; i < logcount; i++ )); do
3196 log=${logs[$i]}
3197 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3198 if (( d < min_date )); then
3199 continue
3200 fi
3201 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3202 hr
3203 fi
3204 done
3205 popd
3206 }
3207
3208
3209 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3210 # especially for links.
3211 profr() {
3212 case $HOSTNAME in
3213 kd)
3214 profr-local
3215 ;;
3216 *)
3217 ssh b8.nz profr-local
3218 ;;
3219 esac
3220 }
3221
3222 profr-local() {
3223 local d0 d1
3224 local -a files
3225 d0="$(date +%Y_%m_%d).log"
3226 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3227 ngset
3228 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3229 ngreset
3230 if (( ${#files[@]} > 0 )); then
3231 cat "${files[@]}" | sort | tail -n 40
3232 fi
3233 }
3234
3235
3236 # Tail pms in the last day, for the case where we restart profanity and
3237 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3238 # If that isn't the case, use prof-recent-local
3239 prof-recent() {
3240 case $HOSTNAME in
3241 kd)
3242 prof-recent-local
3243 ;;
3244 *)
3245 ssh b8.nz prof-recent-local
3246 ;;
3247 esac
3248 }
3249 prof-recent-local() {
3250 local d dates date files f
3251 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3252 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3253 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3254 files=()
3255 for date in ${dates[@]}; do
3256 f=$d/$date.log
3257 if [[ -e $f ]]; then
3258 files+=($f)
3259 fi
3260 done
3261 if (( ${#files[@]} >= 1 )); then
3262 cat ${files[@]} | tail
3263 hr
3264 fi
3265 done
3266 }
3267
3268 prof-sort() {
3269 case $HOSTNAME in
3270 kd)
3271 prof-recent-sort
3272 ;;
3273 *)
3274 ssh b8.nz prof-recent-sort
3275 ;;
3276 esac
3277 }
3278
3279 prof-recent-sort() {
3280 local d dates date files f
3281 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3282 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3283 files=()
3284 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3285 for date in ${dates[@]}; do
3286 f=$d/$date.log
3287 if [[ -e $f ]]; then
3288 files+=($f)
3289 fi
3290 done
3291 done
3292 for f in "${files[@]}"; do
3293 sed "s/\$/ $f/" $f
3294 done | sort
3295 }
3296
3297
3298 # usage: debvm DEBIAN_VERSION RAM_MB
3299 debvm() {
3300 local ver ram fname src
3301 ver=$1
3302 ram=${2:-2024}
3303 # * is because it might have -backports in the name. we only expect 1 expansion
3304 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3305 if (( ${#fnames[@]} >= 2 )); then
3306 echo "error: iank: unexpected multiple files"
3307 return 1
3308 fi
3309 fname="${fnames[0]}"
3310 src=/a/opt/roms/$fname
3311 if [[ ! -f $src ]]; then
3312 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3313 return 1
3314 fi
3315 cp -a $src /t
3316 # note, in fai-revm we do this: not sure why, maybe because of br device
3317 # --graphics spice,listen=0.0.0.0
3318 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3319 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3320
3321 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3322 # but happen to want to try out the debian cloud images. the upstream
3323 # requires python2 and hasn't really changed since the version in d10.
3324 #
3325 # apt install cvs2git cvs
3326 # # 7G was not enough
3327 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3328 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3329 ## www-rsync is an rsynced copy of the cvsfrom savannah
3330 }
3331
3332 mygajim() {
3333 local time time_sec time_pretty days
3334 days=${1:-16}
3335 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
3336 case $time in
3337 16*) : ;;
3338 *) continue ;;
3339 esac
3340 if ! time_pretty=$(date +%F.%R -d @$time); then
3341 echo bad time: $time
3342 return 1
3343 fi
3344 echo $time_pretty "$l"
3345 time_sec=${time%%.*}
3346 # only look at the last 18 days. generally just use this for timesheet.
3347 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3348 done
3349 }
3350
3351 allmygajim() {
3352 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3353 }
3354
3355 gajlogs() {
3356 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3357 }
3358
3359
3360 net-dev-info() {
3361 e "lspci -nnk|gr -iA2 net"
3362 lspci -nnk|gr -iA2 net
3363 hr
3364 e "s lshw -C network"
3365 hr
3366 sudo lshw -C network
3367 }
3368
3369 nk() {
3370 ser stop NetworkManager
3371 ser disable NetworkManager
3372 ser stop NetworkManager-wait-online.service
3373 ser disable NetworkManager-wait-online.service
3374 ser stop dnsmasq
3375 sudo resolvconf -d NetworkManager
3376 # ser start dnsmasq
3377 sudo ifup br0
3378 }
3379 ngo() {
3380 sudo ifdown br0
3381 ser start NetworkManager
3382 sleep 4
3383 sudo nmtui-connect
3384 }
3385
3386 otp() {
3387 oathtool --totp -b "$*" | xclip -selection clipboard
3388 }
3389 # run cmd and copy output
3390 j() {
3391 "$@" |& pee "xclip -r -selection clipboard" cat
3392 }
3393
3394 # xorg copy. copy text piped into command
3395 xc() {
3396 xclip -r -selection clipboard
3397 }
3398 # echo copy
3399 ec() {
3400 pee "xclip -r -selection clipboard" cat
3401 }
3402
3403 pakaraoke() {
3404 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3405 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
3406 }
3407
3408 pfind() { #find *$1* in $PATH
3409 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3410 local pathArray
3411 IFS=: pathArray=($PATH); unset IFS
3412 find "${pathArray[@]}" -iname "*$1*"
3413 }
3414
3415 pick-trash() {
3416 # trash-restore lists everything that has been trashed at or below CWD
3417 # This picks out files just in CWD, not subdirectories,
3418 # which also match grep $1, usually use $1 for a time string
3419 # which you get from running restore-trash once first
3420 local name x ask
3421 local nth=1
3422 # last condition is to not ask again for ones we skipped
3423 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3424 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3425 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3426 read -r -p "$name [Y/n] " ask
3427 if [[ ! $ask || $ask == [Yy] ]]; then
3428 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3429 echo $x | restore-trash > /dev/null
3430 elif [[ $ask == [Nn] ]]; then
3431 nth=$((nth+1))
3432 else
3433 return
3434 fi
3435 done
3436 }
3437
3438
3439 pub() {
3440 rld /a/h/_site/ li:/var/www/iankelling.org/html
3441 }
3442
3443
3444 pumpa() {
3445 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3446 # packages catches up on some changes in future (this is written in
3447 # 4/2017)
3448 #
3449 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3450 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3451 # work area; turns out it\'s impossible to set correctly if you are
3452 # not a fully EWMH compliant desktop environment
3453 #
3454 # geekosaur: chrome shows one failure mode, qt/kde another, other
3455 # gtk apps a third, ... I came up with a setting that works for me
3456 # locally but apparently doesnt work for others, so we joined the
3457 # other tiling window managers in giving up on setting it at all
3458 #
3459 xprop -root -remove _NET_WORKAREA
3460 command pumpa & r
3461 }
3462
3463 # reviewboard, used at my old job
3464 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3465 #rbp() { rbt post -o "$@"; }
3466
3467 rebr() {
3468 sudo ifdown br0
3469 sudo ifup br0
3470 }
3471
3472
3473 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3474 # only run on MAIL_HOST. simpler to keep this on one system.
3475 r2eadd() { # usage: name url
3476 # initial setup of rss2email:
3477 # r2e new r2e@iankelling.org
3478 # that initializes files, and sets default email.
3479 # symlink to the config doesnt work, so I copied it to /p/c
3480 # and then use cli option to specify explicit path.
3481 # Only option changed from default config is to set
3482 # force-from = True
3483 #
3484 # or else for a few feeds, the from address is set by the feed, and
3485 # if I fail delivery, then I send a bounce message to that from
3486 # address, which makes me be a spammer.
3487
3488 r2e add $1 "$2" $1@r2e.iankelling.org
3489 # get up to date and dont send old entries now:
3490 r2e run --no-send $1
3491 }
3492
3493 rspicy() { # usage: HOST DOMAIN
3494 # connect to spice vm remote host. use vspicy for local host
3495 local port
3496 # shellcheck disable=SC2087
3497 port=$(ssh $1<<EOF
3498 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3499 sed -rn "s/.*port='([0-9]+).*/\1/p"
3500 EOF
3501 )
3502 if [[ $port ]]; then
3503 spicy -h $1 -p $port
3504 else
3505 echo "error: no port found. check that the domain is running."
3506 fi
3507 }
3508
3509
3510 scssl() {
3511 # s gem install scss-lint
3512 pushd /a/opt/thoughtbot-guides
3513 git pull --stat
3514 popd
3515 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3516 }
3517
3518 skbrc() {
3519 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3520 }
3521
3522 skaraoke() {
3523 local tmp out
3524 out=${2:-${1%.*}.sh}
3525 tmp=$(mktemp -d)
3526 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3527 # todo, the current sleep seems pretty good, but it
3528 # would be nice to have an empirical measurement, or
3529 # some better wait to sync up.
3530 #
3531 # note: --loop-file=no prevents it from hanging if you have that
3532 # set to inf the mpv config.
3533 # --loop=no prevents it from exit code 3 due to stdin if you
3534 # had it set to inf in mpv config.
3535 #
3536 # args go to mpv, for example --volume=80, 50%
3537 cat >$out <<EOFOUTER
3538 #!/bin/bash
3539 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3540 ( sleep .2; scriptreplay <( cat <<'EOF'
3541 $(cat $tmp/timing)
3542 EOF
3543 ) <( cat <<'EOF'
3544 $(cat $tmp/typescript)
3545 EOF
3546 ))&
3547 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3548 $(base64 "$1")
3549 EOF
3550 kill 0
3551 EOFOUTER
3552 rm -r $tmp
3553 chmod +x $out
3554 }
3555
3556 smeld() { # ssh meld usage host1 host2 file
3557 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3558 }
3559
3560 spd() {
3561 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3562 }
3563
3564 spamf() { # spamtest on FILE
3565 if (( $# != 1 )); then
3566 e spamtest error: expected 1 arg, filename >&2
3567 return 1
3568 fi
3569 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3570 }
3571
3572
3573 # mail related
3574 testmail() {
3575 declare -gi _seq; _seq+=1
3576 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3577 # for testing to send from an external address, you can do for example
3578 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3579 # note in exim, you can retry a deferred message
3580 # s exim -M MSG_ID
3581 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3582 }
3583
3584 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3585 # make modifications, then copy to live file, use -eW to actually modify mailbox
3586 #
3587 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3588 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3589
3590 # sieve with output filter. arg is mailbox, like INBOX.
3591 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3592
3593 # always run this first, edit the test files, then run the following
3594 testsieve() {
3595 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
3596 }
3597 runsieve() {
3598 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3599 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3600 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
3601 }
3602
3603 # usage:
3604 # alertme SUBJECT
3605 # printf "subject\nbody\n" | alertme
3606 alertme() {
3607 if [[ -t 0 ]]; then
3608 exim -t <<EOF
3609 From: alertme@b8.nz
3610 To: alerts@iankelling.org
3611 Subject: $*
3612 EOF
3613 else
3614 read -r sub
3615 { cat <<EOF
3616 From: alertme@b8.nz
3617 To: alerts@iankelling.org
3618 Subject: $sub
3619
3620 EOF
3621 cat
3622 } | exim -t
3623 fi
3624 }
3625 daylertme() {
3626 if [[ -t 0 ]]; then
3627 exim -t <<EOF
3628 From: alertme@b8.nz
3629 To: daylert@iankelling.org
3630 Subject: $*
3631 EOF
3632 else
3633 read -r sub
3634 { cat <<EOF
3635 From: alertme@b8.nz
3636 To: daylert@iankelling.org
3637 Subject: $sub
3638
3639 EOF
3640 cat
3641 } | exim -t
3642 fi
3643 }
3644
3645 # alert when a page goes live.
3646 alert200() {
3647 local quiet url tmpdir
3648 quiet=false
3649 case $1 in
3650 # dont send a diff of the html. some html is not very readable
3651 -q) quiet=true
3652 shift
3653 ;;
3654 esac
3655 url="$1"
3656 tmpdir="$(mktemp -d)"
3657 cd $tmpdir
3658 while true; do
3659 if wget -q "$url"; then
3660 if $quiet; then
3661 echo | daylert 200
3662 else
3663 alertme $tmpdir
3664 fi
3665 fi
3666 sleep $(( 120 + RANDOM % 300 ))
3667 done
3668 }
3669
3670 # alert on changes to a webpage (just the base page that curl gets)
3671 # usage: weblert URL [SUBJECT...]
3672 weblert() {
3673 local u old new quiet
3674 quiet=false
3675 case $1 in
3676 # dont send a diff of the html. some html is not very readable
3677 -q) quiet=true
3678 shift
3679 ;;
3680 esac
3681 u="$1"
3682 shift
3683 subject="${*:-weblert}"
3684 old=$(curl -s "$u") ||:
3685 while true; do
3686 new=$(curl -s "$u") ||:
3687 if [[ $old && $new ]]; then
3688 if [[ $new != "$old" ]]; then
3689 if $quiet; then
3690 echo | daylertme "$subject"
3691 else
3692 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3693 fi
3694 fi
3695 old="$new"
3696 fi
3697 sleep $(( 60 + RANDOM % 120 ))
3698 done
3699 }
3700
3701 torshell() {
3702 # per man torsocks
3703 # shellcheck disable=SC1090 # expected
3704 source "$(type -p torsocks)" on
3705 }
3706
3707 eless2() {
3708 less /var/log/exim4/mymain
3709 }
3710
3711
3712 # mail related
3713 testexim() {
3714 # testmail above calls sendmail, which is a link to exim/postfix.
3715 # its docs dont say a way of adding an argument
3716 # to sendmail to turn on debug output. We could make a wrapper, but
3717 # that is a pain. Exim debug args are documented here:
3718 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3719 #
3720 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3721 # note, for exim daemon, you can turn on debug options by
3722 # adding -d, etc to COMMONOPTIONS in
3723 # /etc/default/exim4
3724 #
3725 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3726 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3727 #
3728 # -t = get recipient from header
3729 exim -d -t <<EOF
3730 From: ian@iankelling.org
3731 To: submit@b.b8.nz
3732 Subject: testbug1
3733
3734 Package: test
3735 Version:1
3736
3737 This is a test message.
3738 EOF
3739 }
3740
3741 # test bounce exim
3742 testbexim() {
3743 to=$1
3744 exim -d -f '<>' $to <<EOF
3745 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3746 To: $to
3747 Subject: Mail delivery failed: returning message to sender
3748
3749 This message was created automatically by mail delivery software.
3750 EOF
3751
3752 }
3753
3754
3755 # toggle keyboard
3756 tk() {
3757 # based on
3758 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3759 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3760 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3761 echo enabling keyboard
3762 # find the first slave keyboard number, they are all the same in my output.
3763 # if they werent, worst case we would need to save the slave number somewhere
3764 # when it got disabled.
3765 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3766 xinput reattach $id $slave
3767 else
3768 xinput float $id
3769 fi
3770 }
3771
3772 tm() {
3773 # timer in minutes
3774 # --no-config
3775 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
3776 }
3777
3778 ## usage: to connect to my main transmission daemon from a different host, run this
3779 trans-remote-route() {
3780 :
3781 }
3782 trg() { transmission-remote-gtk & r; }
3783 # TODO: this wont work transmission.lan doesnt exist
3784 trc() {
3785 # example, set global upload limit to 100 kilobytes:
3786 # trc -u 100
3787 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
3788 }
3789
3790 trysleep() {
3791 retries="$1"
3792 sleepsecs="$2"
3793 shift 2
3794 for (( i=0; i < retries - 1; i++ )); do
3795 if "$@"; then
3796 return 0
3797 fi
3798 sleep $sleepsecs
3799 done
3800 "$@"
3801 }
3802
3803
3804 tu() {
3805 local s
3806 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
3807 s=s;
3808 fi
3809 # full path for using in some initial setup steps
3810 $s /a/exe/teeu "$@"
3811 }
3812
3813 # execute exim in its namespace. Useful args like -Mrm
3814 enn() {
3815 local ecmd pid
3816
3817 ecmd="/usr/sbin/exim4 -C /etc/exim4/my.conf"
3818 if ip a show veth1-mail &>/dev/null; then
3819 s $ecmd "$@"
3820 else
3821 sdncmdroot exim4 $ecmd "$@"
3822 fi
3823 }
3824
3825 # get pid of systemd service
3826 servicepid() {
3827 local pid unit dir
3828 unit="$1"
3829 pid=$(systemctl show --property MainPID --value "$unit")
3830 case $pid in
3831 [1-9]*) : ;;
3832 *)
3833
3834 dir=/sys/fs/cgroup/system.slice
3835 if [[ ! -d $dir ]]; then
3836 # t10 and older directory.
3837 dir=/sys/fs/cgroup/systemd/system.slice
3838 fi
3839
3840 # 0 or empty. This file includes the MainPid, so I expect we
3841 # could just get this in the first place, but i don't know if that
3842 # is always the case.
3843 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
3844 ;;
3845 esac
3846 if [[ $pid ]]; then
3847 printf "%s\n" "$pid"
3848 else
3849 return 1
3850 fi
3851 }
3852
3853 sdnbash() { # systemd namespace bash
3854 local unit pid
3855 if (( $# != 1 )); then
3856 echo $0: error wrong number of args >&2
3857 return 1
3858 fi
3859 unit=$1
3860 pid=$(servicepid $unit)
3861 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
3862 }
3863
3864 sdnbashroot() { # systemd namespace bash as root
3865 local unit pid
3866 if (( $# != 1 )); then
3867 echo $0: error wrong number of args >&2
3868 return 1
3869 fi
3870 unit=$1
3871 pid=$(servicepid $unit)
3872 m sudo nsenter -t $pid -n -m bash
3873 }
3874
3875
3876 # systemd namespace cmd
3877 # usage: UNIT CMD...
3878 sdncmd() {
3879 local unit pid tmpf
3880 if (( $# <= 1 )); then
3881 echo $0: error wrong number of args >&2
3882 return 1
3883 fi
3884 unit=$1
3885 shift
3886 pid=$(servicepid $unit)
3887 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3888 export -p >$tmpf
3889 printf "%s " "${@@Q}" >>$tmpf
3890 echo >>$tmpf
3891 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
3892 }
3893
3894 sdncmdroot() { # systemd namespace root command
3895 local unit pid
3896 if (( $# < 2 )); then
3897 echo $0: error wrong number of args >&2
3898 return 1
3899 fi
3900 unit=$1
3901 shift
3902 pid=$(servicepid $unit)
3903 m sudo nsenter -t $pid -n -m "$@"
3904 }
3905
3906
3907 mailnnbash() {
3908 sdnbash mailnn
3909 }
3910
3911 # we use wireguard now, use mailnnbash.
3912 # mailvpnbash() {
3913 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
3914 # }
3915
3916 eximbash() {
3917 sdnbashroot exim4
3918 }
3919 spamnn() {
3920 local spamdpid
3921 spamdpid=$(systemctl show --property MainPID --value spamassassin)
3922 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
3923 }
3924 unboundbash() {
3925 sdnbashroot unbound
3926 }
3927
3928 nmtc() {
3929 s nmtui-connect "$@"
3930 }
3931
3932 mailnncheck() {
3933 local unit pid ns mailnn
3934 # mailvpn would belong on the list if using openvpn
3935 for unit in mailnn unbound dovecot spamassassin exim4 radicale; do
3936 pid=$(servicepid $unit)
3937 echo debug: unit=$unit pid=$pid
3938 if [[ ! $pid ]]; then
3939 echo failed to find pid for unit=$unit
3940 continue
3941 fi
3942 if ! ns=$(s readlink /proc/$pid/ns/net); then
3943 echo failed to find ns for unit=$unit pid=$pid
3944 continue
3945 fi
3946 if [[ $mailnn ]]; then
3947 if [[ $ns != "$mailnn" ]]; then
3948 echo "$unit ns $ns != $mailnn"
3949 fi
3950 else
3951 mailnn=$ns
3952 fi
3953 done
3954
3955 }
3956
3957
3958 vpncmd() {
3959 sdncmd openvpn-client-tr@client.service "$@"
3960 }
3961 vpni() {
3962 sdncmd openvpn-client-tr@client.service bash
3963 }
3964 vpnbash() {
3965 sdncmdroot openvpn-client-tr@client.service bash
3966 }
3967
3968
3969 vpn() {
3970 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
3971 local vpn_service=openvpn-client
3972 else
3973 local vpn_service=openvpn
3974 fi
3975
3976 [[ $1 ]] || { echo need arg; return 1; }
3977 journalctl --unit=$vpn_service@$1 -f -n0 &
3978 # sometimes the journal doesnt open until after the vpn output
3979 # has happened. hoping this fixes that.
3980 sleep 1
3981 sudo systemctl start $vpn_service@$1
3982 # sometimes the ask-password agent does not work and needs a delay.
3983 sleep .5
3984 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
3985 # noticed around 8-2017 after update from around stretch release
3986 # on debian testing, even though the bug is much older.
3987 sudo systemd-tty-ask-password-agent
3988 }
3989
3990 fixu() {
3991 local stats
3992 ls -lad /run/user/1000
3993 stats=$(stat -c%a-%g-%u /run/user/1000)
3994 if [[ $stats != 700-1000-1000 ]]; then
3995 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
3996 fi
3997 }
3998
3999 # unmute
4000 um() {
4001 local sink card
4002 sink=$(pactl get-default-sink)
4003 if [[ $sink == auto_null ]]; then
4004 # guessing there is just one with an off profile. otherwise we will
4005 # need some other solution, like storing the card identifier that we
4006 # muted with nap.
4007 card=$(pacmd list-cards | sed -n '/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}')
4008 m pacmd set-card-profile "$card" output:analog-stereo
4009 fi
4010
4011 m pactl set-sink-mute @DEFAULT_SINK@ false
4012 rm -f /tmp/ianknap
4013 }
4014
4015 nap() {
4016 local sink card
4017 sink=$(pactl get-default-sink)
4018 card="${sink%.*}"
4019 card="${card/output/card}"
4020 m pacmd set-card-profile "$card" off
4021
4022 # clicking on a link in a browser can cause unmute.
4023 # I don't want that. So, use a stronger form of mute
4024 # than this.
4025 #pactl set-sink-mute @DEFAULT_SINK@ true
4026 touch /tmp/ianknap
4027 }
4028
4029
4030 # systemctl is-enabled / status / cat says nothing, instead theres
4031 # some obscure symlink. paths copied from man systemd.unit.
4032 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
4033 # seru list-dependencies --reverse --all UNIT
4034 sysd-deps() {
4035 local f
4036 local -a dirs search
4037 ngset
4038
4039 case $1 in
4040 u)
4041 search=(
4042 ~/.config/systemd/user.control/*
4043 $XDG_RUNTIME_DIR/systemd/user.control/*
4044 $XDG_RUNTIME_DIR/systemd/transient/*
4045 $XDG_RUNTIME_DIR/systemd/generator.early/*
4046 ~/.config/systemd/user/*
4047 /etc/systemd/user/*
4048 $XDG_RUNTIME_DIR/systemd/user/*
4049 /run/systemd/user/*
4050 $XDG_RUNTIME_DIR/systemd/generator/*
4051 ~/.local/share/systemd/user/*
4052 /usr/lib/systemd/user/*
4053 $XDG_RUNTIME_DIR/systemd/generator.late/*
4054 )
4055 ;;
4056 *)
4057 search=(
4058 /etc/systemd/system.control/*
4059 /run/systemd/system.control/*
4060 /run/systemd/transient/*
4061 /run/systemd/generator.early/*
4062 /etc/systemd/system/*
4063 /etc/systemd/systemd.attached/*
4064 /run/systemd/system/*
4065 /run/systemd/systemd.attached/*
4066 /run/systemd/generator/*
4067 /lib/systemd/system/*
4068 /run/systemd/generator.late/*
4069 )
4070 ;;
4071 esac
4072 for f in "${search[@]}"; do
4073 [[ -d $f ]] || continue
4074 case $f in
4075 *.requires|*.wants)
4076 dirs+=("$f")
4077 ;;
4078 esac
4079 done
4080 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4081 case ${#dirs[@]} in
4082 1)
4083 echo "${dirs[0]}:"
4084 ll "${dirs[@]}"
4085 ;;
4086 0) : ;;
4087 *)
4088 ll "${dirs[@]}"
4089 ;;
4090 esac
4091 ngreset
4092 }
4093
4094 fixvpndns() {
4095 local link istls
4096 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4097 case $istls in
4098 yes|no) : ;;
4099 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4100 esac
4101 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4102 }
4103
4104 vpnoff() {
4105 [[ $1 ]] || { echo need arg; return 1; }
4106 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4107 local vpn_service=openvpn-client
4108 else
4109 local vpn_service=openvpn
4110 fi
4111 sudo systemctl stop $vpn_service@$1
4112 }
4113 vpnoffc() { # vpn off client
4114 ser stop openvpn-client-tr@client
4115 }
4116 vpnc() {
4117 local unit
4118 unit=openvpn-client-tr@client
4119 sudo -v
4120 if [[ $(systemctl is-active $unit) != active ]]; then
4121 s systemctl start $unit
4122 sleep 1
4123 fi
4124 }
4125
4126
4127 vspicy() { # usage: VIRSH_DOMAIN
4128 # connect to vms made with virt-install
4129 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4130 sed -r "s/.*port='([0-9]+).*/\1/")"
4131 }
4132
4133 wian() {
4134 cat-new-files /m/4e/INBOX/new
4135 }
4136 wakehours() {
4137 local sec
4138 if (( $# != 1 )) ; then
4139 echo wakehours: error: expected 1 arg, got $# >&2
4140 return 1
4141 fi
4142 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4143 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4144 }
4145
4146 calvis() { # calendar visualize
4147 install -m 600 /dev/null /tmp/calendar-bytes
4148 while read -r l; do
4149 for char in $l; do
4150 # shellcheck disable=SC2059 # intentional for the hex formatting
4151 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4152 done
4153 done < <(grep -v '[#-]' /p/calendar-data)
4154 /p/c/proc/calendar/linux-amd64/calendar
4155 }
4156
4157 wtr() { curl wttr.in/boston; }
4158
4159 xevkb() { xev -event keyboard; }
4160
4161 # * misc stuff
4162
4163 vrun() {
4164 printf "running: %s\n" "$*"
4165 "$@"
4166 }
4167
4168 electrum() {
4169 # https://electrum.readthedocs.io/en/latest/tor.html
4170 # https://github.com/spesmilo/electrum-docs/issues/129
4171 s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4172 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4173 }
4174 monero() {
4175 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4176 }
4177
4178
4179 # rg my main files
4180 rgm() {
4181 rg "$@" /p/w.org /a/t.org /a/work.org /b
4182 }
4183
4184 # re all my files more expansively
4185
4186 rem() {
4187 local paths
4188 paths="/p/c /b/"
4189 find $paths -not \( -name .svn -prune -o -name .git -prune \
4190 -o -name .hg -prune -o -name .editor-backups -prune \
4191 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4192 rgv $local_rgv_args -g "!bash_unpublished" -- "$*" $paths /a/work.org ||:
4193 }
4194 reml() { # rem with limit to 5 matches per file
4195 local_rgv_args="-m 5"
4196 rem "$@"
4197 }
4198
4199 rep() {
4200 local paths
4201 paths="/p/c"
4202 find $paths -not \( -name .svn -prune -o -name .git -prune \
4203 -o -name .hg -prune -o -name .editor-backups -prune \
4204 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4205 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4206 }
4207 repl() { # rem with limit to 5 matches per file
4208 local local_rgv_args="-m 5"
4209 rem "$@"
4210 }
4211
4212
4213 # re on common fsf files
4214 ref() {
4215 local paths
4216 paths="/f/gluestick /f/brains /f/s /c"
4217 find $paths -not \( -name .svn -prune -o -name .git -prune \
4218 -o -name .hg -prune -o -name .editor-backups -prune \
4219 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4220 rgv -- "$*" $paths /a/work.org ||:
4221 }
4222
4223
4224 # for use in /f/bind
4225 fupzone() {
4226 # shellcheck disable=SC2046 # i want word splitting
4227 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4228 }
4229
4230 # setup:
4231 # pip3 install linode-cli
4232 # linode-cli
4233 livp9() {
4234 local input ip id tmp
4235 input=$1
4236 if [[ $2 ]]; then
4237 id=$2
4238 ip=$3
4239 else
4240 tmp=$(mktemp)
4241 echo $tmp
4242 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4243 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4244 for string in $ip $id; do
4245 case $string in
4246 [0-9]*) : ;;
4247 *)
4248 echo "livp9: bad value ip=$ip id=$id input=$input"
4249 return 1
4250 ;;
4251 esac
4252 done
4253 rm $tmp
4254
4255 while true; do
4256 if timeout 4 ssh $ip :; then
4257 break
4258 fi
4259 sleep 3
4260 done
4261 fi
4262 ssh $ip <<EOF
4263 apt-get -qq update
4264 apt-get -qq -y install ffmpeg rsync
4265 mkdir vp9
4266 EOF
4267 m rsync $input $ip:
4268 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
4269 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
4270 rsync $ip:vp9/$input vp9
4271 linode-cli linodes delete $id
4272 }
4273
4274 reset-konsole() {
4275 # we also have a file in /a/c/...konsole...
4276 local f=$HOME/.config/konsolerc
4277 setini DefaultProfile profileian.profile "Desktop Entry" $f
4278 setini Favorites profileian.profile "Favorite Profiles" $f
4279 setini ShowMenuBarByDefault false KonsoleWindow $f
4280 setini TabBarPosition Top TabBar $f
4281 }
4282
4283 reset-sakura() {
4284 while read -r k v; do
4285 # shellcheck disable=SC2154
4286 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4287 done <<'EOF'
4288 colorset1_back rgb(33,37,39)
4289 less_questions true
4290 audible_bell No
4291 visible_bell No
4292 disable_numbered_tabswitch true
4293 scroll_lines 10000000
4294 scrollbar true
4295 EOF
4296 }
4297
4298 # make a page of links found in the files $@. redirect output
4299 linkhtml() {
4300 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4301 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4302 }
4303
4304 reset-xscreensaver() {
4305 # except for spash, i set these by setting gui options in
4306 # xscreensaver-command -demo
4307 # then finding the corresponding option in .xscreensaver
4308 # spash, i happened to notice in .xscreensaver
4309 #
4310 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4311 cat > /home/iank/.xscreensaver <<'EOF'
4312 mode: blank
4313 dpmsEnabled: True
4314 dpmsStandby: 0:07:00
4315 dpmsSuspend: 0:08:00
4316 dpmsOff: 0:00:00
4317 timeout: 0:05:00
4318 lock: True
4319 lockTimeout: 0:06:00
4320 splash: False
4321 EOF
4322
4323 }
4324
4325
4326 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4327 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4328
4329
4330 # * stuff that makes sense to be at the end
4331 if [[ "$SUDOD" ]]; then
4332 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4333 cd "$SUDOD" ||:
4334 unset SUDOD
4335 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4336 cd /a
4337 OLDPWD=
4338 fi
4339
4340
4341
4342
4343 # for mitmproxy to get a newer python.
4344 # commented until i want to use it because it
4345 # noticably slows bash startup
4346 #
4347
4348 mypyenvinit () {
4349 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4350 echo "error: dont be root. make sure pyenv is installed"
4351 return 1
4352 fi
4353 export PATH="$HOME/.pyenv/bin:$PATH"
4354 eval "$(pyenv init -)"
4355 eval "$(pyenv virtualenv-init -)"
4356 }
4357
4358
4359
4360 # I have the git repo and a release. either one should work.
4361 # I have both because I was trying to solve an issue that
4362 # turned out to be unrelated.
4363 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4364
4365 ## i should have documented this...
4366 # based on https://github.com/keyboardio/Kaleidoscope
4367 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4368
4369 # They want to be added to the start, but i think
4370 # that should be avoided unless we really need it.
4371 path-add --end ~/.npm-global
4372
4373
4374 path-add --end $HOME/.cargo/bin
4375
4376 if type -P rg &>/dev/null; then
4377 # --no-messages because of annoying errors on broken symlinks
4378 # -z = search .gz etc files
4379 # -. = search dotfiles
4380 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 $?; }
4381 #fails if not exist. ignore
4382 complete -r rg 2>/dev/null ||:
4383 else
4384 alias rg=grr
4385 fi
4386
4387 # rg with respecting vcs ignore files
4388 rgv() {
4389 ret=0
4390 # settings that are turned off for pipes, keep them on.
4391 # Found by searching for "terminal" in --help
4392 # --heading
4393 # -n
4394 #
4395 # -. = search dotfiles
4396 # -z = search zipped files
4397 # -i = case insensitive
4398 # -M = max columns
4399 # --no-messages because of annoying errors on broken symlinks
4400 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4401 command rg -n --heading -. -z --no-messages -i -M 900 --no-ignore-parent -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" || ret=$?
4402 return $ret
4403 }
4404
4405 amall() {
4406 echo "$(tput setaf 5 2>/dev/null ||:)█ coresite █$(tput sgr0 2>/dev/null||:)"
4407 amfsf "$@"
4408 echo "$(tput setaf 5 2>/dev/null ||:)█ office █$(tput sgr0 2>/dev/null||:)"
4409 amoffice "$@"
4410 }
4411 amallq() { # amall quiet
4412 amfsf "$@"
4413 amoffice "$@"
4414 }
4415 amfsf() {
4416 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4417 amtool "$@"
4418 }
4419 amoffice() {
4420 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4421 amtool "$@"
4422 }
4423 amls() {
4424 amall silence query "$@"
4425 }
4426 # amtool silence add
4427 amsa() {
4428 amall silence add "$@"
4429 }
4430 # amtool silence force
4431 amsf() {
4432 amall silence add x!="1"
4433 }
4434 amrmall() {
4435 # note: not sure if quoting of this arg is correct
4436 amfsf silence expire "$(amfsf silence query -q)"
4437 amoffice silence expire "$(amoffice silence query -q)"
4438 }
4439
4440
4441 youtube-dl-update() {
4442 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4443 sudo chmod a+rx /usr/local/bin/youtube-dl
4444 }
4445
4446 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4447 yt-dlp-update() {
4448 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4449 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4450 }
4451
4452 mpvyt() {
4453 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4454 }
4455
4456 # taken from default changes to bashrc and bash_profile
4457 path-add --end --ifexists $HOME/.rvm/bin
4458 # also had ruby bin dir, but moved that to environment.sh
4459 # so its included in overall env
4460
4461
4462 # ya, hacky hardcoded hostnames in 2023. we could do better
4463 hssh-update() {
4464 local -a failed_hosts hosts
4465 case $HOSTNAME in
4466 sy|kd)
4467 hosts=(
4468 kd x3.office.fsf.org syw
4469 )
4470 ;;
4471 x3)
4472 hosts=(
4473 b8.nz sywg.b8.nz
4474 )
4475 ;;
4476 esac
4477 for host in ${hosts[@]}; do
4478 e $host
4479 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4480 failed_hosts+=($host)
4481 fi
4482 done
4483 if (( ${#failed_hosts[@]} >= 1 )); then
4484 echo failed_hosts=${failed_hosts[*]}
4485 return 1
4486 fi
4487 }
4488
4489 noi3bar() {
4490 touch /tmp/noi3bar
4491 }
4492 i3bar() {
4493 rm -fv /tmp/noi3bar
4494 }
4495
4496 # example:
4497 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4498 #
4499 attach-txt() {
4500 local f
4501 for f; do
4502 if [[ ! -s $f ]]; then
4503 e "error: empty or non-existent file $f"
4504 return 1
4505 fi
4506 done
4507 for f; do
4508 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4509 done | ec
4510 }
4511
4512 ctof() {
4513 units "tempC($1)" tempF
4514 }
4515
4516 ftoc() {
4517 units "tempF($1)" tempC
4518 }
4519
4520 # requires dns/firewall setup first
4521 local-icecast() {
4522 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4523 <Location "/fsf.webm">
4524 AuthType Basic
4525 AuthName "basic_auth"
4526 # created with
4527 # htpasswd -c icecast-fsf-htpasswd USERNAME
4528 AuthUserFile "/etc/icecast-fsf-htpasswd"
4529 Require valid-user
4530 </Location>
4531 <Location "/fsf-tech.webm">
4532 AuthType Basic
4533 AuthName "basic_auth"
4534 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4535 Require valid-user
4536 </Location>
4537 EOF
4538 }
4539
4540 # obs screen switching of
4541 obof() {
4542 ls -l /tmp/no-obs-auto-scene-switch
4543 touch /tmp/no-obs-auto-scene-switch
4544 }
4545 # obs screen switching on
4546 obon() {
4547 ls -l /tmp/no-obs-auto-scene-switch
4548 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4549 rm -f /tmp/no-obs-auto-scene-switch
4550 fi
4551 }
4552
4553 obs-gen-profiles() {
4554 local p=/p/c/basic/profiles
4555 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4556 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4557 }
4558
4559 # terminal clear. like clear, but put the prompt at the bottom,
4560 # useful for obs streaming the bottom half of a terminal window.
4561 tclear() {
4562 for ((i=0; i<COLUMNS; i++)); do
4563 echo
4564 done
4565 }
4566
4567
4568 export BASEFILE_DIR=/a/bin/fai-basefiles
4569
4570 #export ANDROID_HOME=/a/opt/android-home
4571 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
4572 #export USE_SDK_WRAPPER=yes
4573 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
4574
4575 # didnt get drush working, if I did, this seems like the
4576 # only good thing to include for it.
4577 # Include Drush completion.
4578 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
4579 # source /home/ian/.drush/drush.complete.sh
4580 # fi
4581
4582
4583 # best practice
4584 unset IFS
4585
4586 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
4587 # i added an extra condition as gentoo xorg guide says depending on
4588 # $DISPLAY is fragile.
4589 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
4590 exec startx
4591 fi
4592
4593
4594 # ensure no bad programs appending to this file will have an affect
4595 return 0