Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

My new choice of clipboard manager: parcellite

I used CopyQ, but there is some sort of bug in QT-based projects where it doesn't accept keyboard input; it's like it doesn't receive focus until you minimize or maximize or some baloney that finally got me irritated enough to do something about it. Maybe I'll check CopyQ in the future, in case the bugs are ever solved or something. I have never researched this with any results; perhaps I don't know how to explain the phenomenon.

Anyway, I settled on parcellite for the time being. Let's face it; I'm not doing any more research so this is going to be my long-term clipboard solution. It's gtk3-based, which is my current stack anyways. It has all the options I want: a popup menu of history options, with pinnable entries (called persistent entries in this tech), and is already packaged for Devuan.

Notes on other clipboard managers

It appears that diodon and gpaste are forks or spiritual successors to parcellite. Gpaste is the freedesktop-ified thing with a daemon and complicated features that just seem extraneous, yet is still missing features I want.

Raw notes for research process

  1. determine goals
    1. ctrl+shift+z pulls up short history menu, approx 5.
    2. full history available from a window from a menu option
  2. find options
    1. checking https://wiki.archlinux.org/title/Clipboard?useskin=vector
    2. already packaged for Devuan is a huge plus
    3. packagable for devuan is acceptable
  3. test top option(s)
    1. diodon: works basically. No "full history" dialog, and no pinning of items.
    2. gpaste: way more complicated, but has an editable history, from gpaste-client ui. SCARY "upload" feature that does what?
    3. parcellite (github or sourceforge)

My current desired config

Here is my current ~/.config/parcellite/parcelliterc file as of the time of this writing.

files/2025/listings/parcelliterc (Source)

[rc]
RCVersion=1
use_copy=true
use_primary=false
synchronize=false
save_history=true
history_pos=false
history_x=1
history_y=1
history_limit=25
data_size=0
item_size=5
automatic_paste=true
auto_key=true
auto_mouse=false
key_input=false
restore_empty=true
rc_edit=true
type_search=true
case_search=false
ignore_whiteonly=false
trim_wspace_begend=false
trim_newline=false
hyperlinks_only=false
confirm_clear=true
current_on_top=true
single_line=true
reverse_history=false
item_length=50
persistent_history=true
persistent_separate=false
persistent_on_top=false
persistent_delim=\\n
nonprint_disp=false
ellipsize=2
multi_user=true
icon_name=parcellite
menu_key=
history_key=<CTRL><SHIFT>Z
phistory_key=<CTRL><ALT>Z
actions_key=

Trying owntracks to track my mobile devices

I was reading some discussion on Hacker News and the fine folks there mentioned Dawarich, a self-hostable server for managing your location data. I got interested in this sort of thing. I've never really used stuff like this before for myself, even from the big names. I use Google Maps for navigation, but not for showing myself my location history. I'm sure the setting for "do not record my location" merely turns off my ability to look it up; I'm sure Google still knows exactly where my mobile device has been (that is not the same as where I have been) across all of time.

But F-Droid didn't have the Dawarich app, but it had the app for owntracks, a similar tool.

Stackrpms installation of owntracks

This application is for tracking my location history on my own infrastructure.

Goals

I want to be able to view my location history. In the past, I used Google Maps location history to find where I had stayed on a particular vacation. I want to be able to do this sort of auditing without depending on external vendors.

Setup

This application is installed in docker on server4.

sudo useradd owntracks
sudo usermod -a -G docker owntracks
sudo su - owntracks
mkdir -p compose ; cd compose

I established file docker-compose.yml and config.js. I just picked random ports available, including server4:8083 for recorder, and server4:8085 for frontend.

On server3, set up custom apache httpd config owntracks.cnf which is an adaptation of [Reference #5][5]. Adapt local_mirror.conf to include this for VirtualHosts 443 (internal https) and 444 (external https).

Establish new password repository. Use password scheme 3j.

# as root@server3
htpasswd -c .owntracks device5
htpasswd -c .owntracks device4

Added a port in the firewall on server4.

sudo firewall-cmd --add-port=8083/tcp
sudo firewall-cmd --add-port=8085/tcp
sudo firewall-cmd --add-port=1883/tcp
sudo firewall-cmd --add-port=8883/tcp

I established the container mosquitto, which uses its own TLS cert. The openssl req needed to be run on a newer system where parameter -addext can be used. I ran the ipa cert-request from server4 which owns the service we add here.

openssl genpkey -algorithm RSA -out https-server4.ipa.internal.com.key
openssl req -new -key https-server4.ipa.internal.com.key -subj "/O=IPA.INTERNAL.COM/CN=server4.ipa.internal.com" -addext "subjectAltName = DNS:server4.ipa.internal.com,DNS:www.example.com" -out https-server4.ipa.internal.com.csr
ipa service-add --force HTTP/server4.ipa.internal.com
ipa cert-request --principal=HTTP/server4.ipa.internal.com https-server4.ipa.internal.com.csr --certificate-out=https-server4.ipa.internal.com.pem

I configured the main router to forward port 8883/tcp to server4:8883. There might be a way to use an apache httpd reverse proxy for websockets to mqtt, but I have not investigated that.

To establish the mosquitto password database, use -c in the command, inside the container.

mosquitto_passwd -c /mosquitto/config/password_file recorder

The rest of the commands will omit -c.

The mosquitto config is in file mosquitto-conf/mosquitto.conf, and uses TLS.

Establishing useful contact info with cards

I want to be able to see pretty names/faces for "friends". https://owntracks.org/booklet/features/card/

I Used https://github.com/owntracks/recorder/raw/refs/heads/master/contrib/faces/image2card.sh from https://github.com/owntracks/recorder/blob/master/contrib/faces/image2card.sh to do this:

sh image2card.sh user1.jpg "user1" "na" > device5-device5.json
sh image2card.sh user2.png "user2" "nn" > device4-device4.json
sh image2card.sh user3.png "user3" "eo" > device2-device2.json

I prepared these so they are accessible for container mosquitto in /mosquitto/config/cards/, and then entered the container and ran these commands.

docker-compose exec mosquitto /bin/sh
mosquitto_pub -L 'mqtt://recorder:KEEPASS@localhost:1883/owntracks/device5/device5/info' -f /mosquitto/config/cards/device5-device5.json -r -q 2 -d

To remove/delete a card:

mosquitto_pub -L 'mqtt://recorder:KEEPASS@localhost:1883/owntracks/device5/device5/info' -n -r -d

Associated files

On server server4.

  • /home/owntracks/compose
    • config.js, taken from official example https://github.com/owntracks/frontend/blob/main/public/config/config.example.js for frontend.
    • docker-compose.yml, very simple combination of official compose example files for recorder (backend) and frontend.
    • mosquitto-conf/password_file
    • mosquitto-conf/mosquitto.conf
    • mosquitto-conf/https-server4.ipa.internal.com.key
    • mosquitto-conf/https-server4.ipa.internal.com.pem
    • mosquitto-conf/ca-ipa.internal.com.pem
    • mosquitto-conf/cards directory of custom .json files for "cards" that describe the various devices, and the script that loads them

      !/bin/sh

      cd "${1}" for word in *.json ; do echo "${word%%.json}" | while IFS='-' read a b ; do mosquitto_pub -L "mqtt://recorder:KEEPASS@localhost:1883/owntracks/${a}/${b}/info" -f "${word}" -r -q 2 -d ; done ; done

On server server3, the main web server.

  • /etc/httpd/conf.d/owntracks.cnf
  • /etc/httpd/conf.d/local_mirror.conf includes owntracks.cnf in virtualhosts 443 and 444.
  • /etc/httpd/.owntracks (or similar) for the credential.

Associated urls

Operations

Restarting the containers

Connect to owntracks@server4.

docker-compose pull ; docker-compose down ; docker-compose up -d

Adding a new device

Adding a device for MQTT auth

Connect to the mosquitto container and then modify the existing password file. The username will probably be the device name.

docker-compose exec mosquitto /bin/sh
mosquitto_passwd /mosquitto/config/password_file <username>
<password>

I am not sure if container mosquitto needs to be restarted to take effect.

Configure the client settings Connection.

Mode=MQTT
Host=www.example.com
Port=8883
Client ID=<device name>
Use Websockets=True
Device ID=<device name>
Tracker ID=?????
Username=<from password_file>
Password=<from password_file>
TLS=True

Make sure the android device trusts the CA cert, which is available at https://server3/internal/certs/ca-ipa.internal.com.crt

Adding a device for http auth

WARNING I think this might work, but it also might not work anymore now with MQTT usage.

I have decided to use a separate http credential for each device. Visit server3 and set up a new credential in the password file. This example shows adding device4.

sudo htpasswd -c /etc/httpd/.owntracks device4
<enter a password, probably "KEEPASS">

It is not necessary to reload the web server. Then configure OwnTracks on the new device to use this http endpoint.

URL=https://www.example.com/owntracks/pub
Device ID=device4
Tracker ID=<anything will do fine>
Username=device4
Password=<same as from htpasswd command>

Removing data about an old user/device combo.

To remove user "user" device "device5", run this command:

# on server4
sudo rm -rf /home/owntracks/compose/owntracks-recorder/store/last/user/device5

I am not sure if this removes info in the frontend for this user+device.

Viewing server app logs

docker-compose logs -f otrecorder mosquitto

Viewing location history

Visit http://server4:8085/ which is container frontend.

Re-adding the cards

A card is the pretty info about a device, such as user name and picture. These are hand-curated with a useful script (search image2card.sh in the install section), and after a container restart, you need to reload these.

docker-compose exec mosquitto /mosquitto/config/cards/import-cards /mosquitto/config/cards

References

  1. Self-Hosted device tracking with OwnTracks | Brian Douglass
  2. owntracks/recorder - Docker Image | Docker Hub
  3. owntracks/frontend: 🌍 Web interface for OwnTracks built with Vue.js
  4. owntracks/frontend - Docker Image | Docker Hub
  5. owntracks/reverse-proxy.conf at main · l33tn00b/owntracks
  6. mqtt - How to configure two Mosquitto listeners with different protocols? - Stack Overflow
  7. Latest way to get certificate in FreeIPA | Knowledge Base
  8. How to setup selfhosted owntracks server
  9. Card - OwnTracks Booklet
  10. Friends - OwnTracks Booklet
  11. mosquitto_pub delete topic at DuckDuckGo

Alternate reading

In case I want to experiment with using httpd to reverse-proxy MQTT or websockets.

  1. https://www.rushworth.us/lisa/?p=358
  2. proxy - Mosquitto + Apache - Super User

    <VirtualHost *:1883>
            ProxyRequests Off
            ProxyPreserveHost On
            ProxyPass /mqtt ws://$Broker-IP:$Broker-Port
            ProxyPassReverse /mqtt ws://$Broker-IP:$Broker-Port
    </VirtualHost>
    
  3. n8n apache2 reverse proxy fix websockets – 🔐 Karlo Luiten

    <IfModule mod_ssl.c>
    <VirtualHost *:443>
        SSLEngine off
        ServerAdmin webmaster@karloluiten.nl
        ServerName n8n.karloluiten.nl
        DocumentRoot "/var/www/html/"
        ErrorLog "${APACHE_LOG_DIR}/error_n8n.servar_nl.log"
        CustomLog ${APACHE_LOG_DIR}/access_n8n.servar_nl.log combined
    
        RequestHeader set X-Forwarded-Proto https
        RemoteIPHeader X-Forwarded-For
        RequestHeader set X-Forwarded-Host "%{SERVER_NAME}e"
    
        ProxyPreserveHost Off
        ProxyPass        / http://10.0.40.54:5678/
        ProxyPassReverse / http://10.0.40.54:5678/
    
        RewriteEngine On
        RewriteCond %{HTTP:Upgrade} =websocket [NC]
        RewriteRule /(.*)           ws://10.0.40.54:5678/$1 [P,L]
        RewriteCond %{HTTP:Upgrade} !=websocket [NC]
        RewriteRule /(.*)           http://10.0.40.54:5678/$1 [P,L]
        ProxyPassReverse /          https://n8n.karloluiten.nl
        SSLCertificateFile /etc/letsencrypt/live/n8n.karloluiten.nl/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/n8n.karloluiten.nl/privkey.pem
        Include /etc/letsencrypt/options-ssl-apache.conf
    </VirtualHost>
    </IfModule>
    

    Obviously fill in your own details/domains/ip.

    Also for env vars:

    N8N_BASIC_AUTH_ACTIVE=true
    N8N_BASIC_AUTH_USER=admin
    N8N_BASIC_AUTH_PASSWORD=xxx
    N8N_SECURE_COOKIE=false
    WEBHOOK_URL=https://n8n.karloluiten.nl/
    N8N_PROXY_HOPS=1
    
  4. Reverse Proxying WebSockets through mod_proxy — HTTP Failback – Lisa's Home Page

    I’ve been successfully reverse proxying MQTT over WebSockets via Apache HTTPD for quite some time now. The last few weeks, my phone isn’t able to connect. There’s no good rational presented (and manually clicking the “send data” button on my client successfully connects). It was time to upgrade the server anyway. Once I got the latest Linux distro on the server, I couldn’t connect at all to my MQTT server. The error log showed AH00898: Error reading from remote server returned by /mqtt

    Evidently, httpd 2.4.47 added functionality to upgrade and tunnel in accordance with RFC 7230. And that doesn’t work at all in my scenario. Haven’t dug in to the why of it yet, but adding ProxyWebsocketFallbackToProxyHttp Off to the reverse proxy config allowed me to successfully communicate with the MQTT server through the reverse proxy.

cli to mute one application

This isn't the biggest deal in the world, but I wanted to toggle a single program's audio without having to navigate the pulseaudio menus/applications. I wanted to mute Civ2.

files/2025/listings/mute-one-application.sh (Source)

1
2
3
#!/bin/sh
index="$( pactl --format=json list sink-inputs | jq ".[]? | select(.properties.\"application.name\" | startswith(\"civ2\")) | .index" )
pactl set-sink-input-mute "${index}" toggle

Find the sink-input associated with the exact process I am interested in, and then toggle the mute.

XDG_CONFIG_HOME for Python

Here is a python oneliner for loading an application config from the recommended path, because I've needed it at least twice now, and I'll need it again in the future.

# Use APPNAME_CONF file if defined, otherwise, use $XDG_CONFIG_HOME/appname.conf, and use ~/.config if XDG_CONFIG_HOME is not defined
conffile = os.environ.get("APPNAME_CONF",os.path.join(os.environ.get("XDG_CONFIG_HOME",os.path.join(os.getenv("HOME"),".config")),"appname.conf"))

Using configparser is left as an exercise for the reader.

Walkthrough my Luanti config

Of course a Luanti config will vary from server to server. Here's mine, and my reasons.

  • server_address is a good example of how to use my customizations to project Server List. My docker container exposes whatever the default port would be, but my router exposes a different port. My internal server list parses out the server_address port and uses that to advertise this Luanti server.
  • remote_media depends on a basic remote media server which is not hard to do.

Read more…

Troubleshooting Darkflame Lego Universe logins

I spent about 3 hours troubleshooting my Darkflame Lego Universe server. My users could not connect. I turned the server back on after a few months of being offline, and nobody could connect. I wanted to test the new Chat Web API, and yes, I know it was limited to localhost specifically.

So I had 2 problems to solve: use the chat web api to list active players, and also fix the server so people can use it.

I could not confirm inside the docker container that the port was listening. There was no ss or netstat installed, so I searched and found a guide that provided this useful snippet.

Read more…

My version of remote-media script for Luanti

Backstory

There is a very good doc about setting up a remote media server for Luanti, which is a web server that hosts the hash-named media files that a client will need to download for playing on a server. It reminds me of c_rehash for x.509 certs for openssl. Using a web server for these files lets you take advantage of static file hosting like in nginx or httpd, which is faster than the UDP connections that Luanti uses. Luanti can do it, but nginx will be faster.

There is a simple demo script on that docs page, and it is a great reference point.

My version

files/2025/listings/generate-remote-media.sh (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/bin/sh
# File: generate-remote-media.sh
# Location: /mnt/public/Support/Games/luanti/scripts/
# Author: sofar, bgstack15
# Startdate: 2025-09-26-6 11:15
# Title: Generate Remote Media
# Project: Luanti Remote Media
# Purpose: Generate Remote Media Directory for Web Hosting
# History:
# Usage:
#    Server setup: minetest.conf `remote_media = https://www.example.com/games/luanti/media/`
#    RECURRING:
#    on vm4: time INDIR=/home/luanti/game-avengers1 OUTDIR=/mnt/public/www/games/luanti/media /mnt/public/www/games/luanti/scripts/generate-remote-media.sh
#    on server3: . /mnt/public/Support/Games/luanti/scripts/generate-remote-media.sh ; time index_dir /mnt/public/www/games/luanti/media
# Reference: https://docs.luanti.org/for-server-hosts/remote-media/
# Improve:
# Documentation:
#    Using a remote media server will crash the client if the client is running with --verbose and piping to grep | tee.
#    The client can remote-fetch .obj, .tr, and .po files, but the reference script does not demonstrate these. This script omits .tr and .po translation files because I do not expect to need them.

collect_from() {
    _indir="${1}"
    _outdir="${2}"
    _count="${3}"
    _start="${4}"
    x="${_start}"
    _temp="$( mktemp )"
    printf '' > "${_temp}"
    clean_collect_from() {
        rm -f "${_temp:-NOTHINGTODEL}"
        trap '' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20
    }
    trap "__ec=$? ; clean_collect_from ; trap '' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 ; exit ${__ec} ;" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20
    {
        echo "Processing media from: ${_indir}"
        find -L "${_indir}" -type f -name "*.png" -o -name "*.obj" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.ogg" -o -name "*.x" -o -name "*.b3d" | while read f; do
            x=$((x+1))
            printf "%0${#_count}d/%0${#_count}d " "${x}" "${_count}" ; basename "$f"
            hash=`openssl dgst -sha1 <"$f" | cut -d " " -f 2`
            cp "$f" "${_outdir}/$hash"
            printf '%s' "." >> "${_temp}"
        done
    } 1>&2
    wc -c "${_temp}" | awk '{print $1}'
    clean_collect_from
}

index_dir() {
    # Usage: index_dir "${OUTDIR}"
    _outdir="${1}"
    printf "Creating index.mth... "
    printf "MTHS\x00\x01" > "${_outdir}/index.mth"
    find "${_outdir}" -type f -not -name index.mth | while read f; do
        openssl dgst -binary -sha1 <$f >> "${_outdir}/index.mth"
    done
}

main() {
    INDIR="${INDIR:-/home/luanti/game-avengers1}"
    OUTDIR="${OUTDIR:-/mnt/public/www/games/luanti/media}"
    mkdir -p "${OUTDIR}"
    # Change this 'collect_from' or add more lines of 'collect_from', the script will recursively
    # search for files with extensions of Luanti media in this folder.
    # This is an example to be run in a game folder, but you can change this to anything to catch
    # all textures that a server uses.
    # My indir will be the /home/luanti/game-avengers1/ style directory, so we only need mods and games. I do not use textures in my worlds.
    _count="$( find -L "${INDIR}/mods" "${INDIR}/games" -type f -name "*.png" -o -name "*.obj" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.ogg" -o -name "*.x" -o -name "*.b3d" | wc -l )"
    _current="$( collect_from "${INDIR}/mods" "${OUTDIR}" "${_count}" 0 )"
    echo "Got result _current ${_current}"
    _current="$( collect_from "${INDIR}/games" "${OUTDIR}" "${_count}" "${_current}" )"
    # Example for MineClone2 (they put textures in textures/ now)
    #collect_from mods/
    #collect_from textures/
    # You should run index_dir on the server that owns the hashed filenames. Doing it over nfs is slower.
    #index_dir "${OUTDIR}"
}

# BEGIN IS-SOURCED
sourced=0
if [ -n "$ZSH_VERSION" ]; then 
   case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
elif [ -n "$KSH_VERSION" ]; then
   [ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ] && sourced=1
elif [ -n "$BASH_VERSION" ]; then
   (return 0 2>/dev/null) && sourced=1 
else # All other shells: examine $0 for known shell binary filenames.
   # Detects `sh` and `dash`; add additional shell filenames as needed.
   case ${0##*/} in sh|-sh|dash|-dash) sourced=1;; esac
fi
# END BEGIN IS-SOURCED
if test "${sourced}" = "0" ;
then
    main
fi

This script is designed to be run on the Luanti server, and on the separate web server. Hashing a few thousand files works way better when doing it against a local filesystem than on nfs!

I do not include the .tr and .po translation files, which are eligible for being pulled from a remote media server, because I do not need them. It triples (as in from 3,000 objects to 9,000 objects) the number of assets I needed to prepare, so it's an optimization I made for myself.

So on the game server, for each instance/world, I run the following.

time INDIR=/home/luanti/game-avengers1 OUTDIR=/mnt/public/www/games/luanti/media /mnt/public/www/games/luanti/scripts/generate-remote-media.sh

And then on the one web server, just once, I run:

. /mnt/public/Support/Games/luanti/scripts/generate-remote-media.sh ; time index_dir /mnt/public/www/games/luanti/media

Using a remote media server

And then of course to use this, you need the clients configured to use remote media server in minetest.conf.

# Default = true
enable_remote_media_server = true

And the game server needs to tell the clients where to get such media, also in minetest.conf.

# default is empty. Be sure to end with a slash here.
remote_media = http://www.example.com/games/luanti/media/

Additional notes

Luanti contributions, part 5

Here is a summary of what I've been up to in the Luanti world since the last time.

Read more…

New Luanti mod: privs_per_world

I wrote a novel mod for Luanti, specifically for Mineclonia. This mod is named privs_per_world (codeberg, ContentDB) and it is an engine for automatically granting and revoking privileges based on entering and leaving the various worlds (Overworld, Nether, End) of Mineclonia.

The default settings grant fast, fly, noclip to players entering the Nether, and revokes these privileges upon leaving.

The server admin can configure settings in minetest.conf. See settingtypes.txt, but it is easier to use settings.lua.

I know that the Mineclonia guys tend to adapt existing mods, so if there's some "worlds" mod that facilitates different "dimensions" and the transport to and from them, this might be useful elsewhere but I don't have any test environments for that.

Here is the default settings, which are useful for a Mineclonia survival server.

ppw.settings = {
    nether = {
        enter = { 
            give = core.settings:get("privs_per_world.nether_enter_give") or "fly,noclip,fast",
            take = core.settings:get("privs_per_world.nether_enter_take") or "",
        },
        leave = {
            give = core.settings:get("privs_per_world.nether_leave_give") or "",
            take = core.settings:get("privs_per_world.nether_leave_take") or "fly,noclip,fast",
        },
    },
    overworld = {
        enter = {
            give = core.settings:get("privs_per_world.overworld_enter_give") or "",
            take = core.settings:get("privs_per_world.overworld_enter_take") or "",
        },
        leave = {
            give = core.settings:get("privs_per_world.overworld_leave_give") or "",
            take = core.settings:get("privs_per_world.overworld_leave_take") or "",
        },
    }
}

Future improvements

I might need to use mod storage to store privileges of user before entering/leaving the world, so we can restore them. But this would lose any privileges that change while the user is in the world. So register_on_grant_priv might be useful, if this mod's own set_privileges does not trigger those.