[bin] Put bin files in .local/bin
This commit is contained in:
352
bin/.local/bin/bing-wallpaper.sh
Executable file
352
bin/.local/bin/bing-wallpaper.sh
Executable file
@ -0,0 +1,352 @@
|
||||
#!/bin/sh
|
||||
PATH=/usr/local/bin:/usr/local/sbin:~/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
readonly SCRIPT=$(basename "$0")
|
||||
readonly VERSION='1.1.1'
|
||||
RESOLUTIONS=(1920x1200 1920x1080 1024x768 1280x720 1366x768 UHD)
|
||||
MONITOR="0" # 0 means all monitors
|
||||
PLIST_FILE="$HOME/Library/LaunchAgents/com.bing-wallpaper-daily-mac-multimonitor"
|
||||
AUTO_UPDATE_NAME="default"
|
||||
ARGS=$@
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
$SCRIPT [options]
|
||||
$SCRIPT -h | --help
|
||||
$SCRIPT --version
|
||||
|
||||
Options:
|
||||
enable-auto-update Enable automatic update of wallpapers every day
|
||||
the picture if the filename already exists.
|
||||
disable-auto-update Disable automatic update of wallpapers every day
|
||||
the picture if the filename already exists.
|
||||
info Show description of current wallpaper.
|
||||
--auto-update-name <name> Name of your auto update when enabling/disabling
|
||||
Using custom name enables setting multiple automatic update configurations.
|
||||
Eg. Set on monitor 1 todays wallpaper and on monitor 2 wallpaper from yesterday
|
||||
-f --force Force download of picture. This will overwrite
|
||||
the picture if the filename already exists.
|
||||
-s --ssl Communicate with bing.com over SSL.
|
||||
-q --quiet Do not display log messages.
|
||||
-c --country <coutry-tag> Specify market country/region eg. en-US, cs-CZ
|
||||
Pictures may be different for markets on some days.
|
||||
See full list of countries on https://learn.microsoft.com/en-us/previous-versions/bing/search/dd251064(v=msdn.10)
|
||||
-d --day <number> Day for which you want to get the picture.
|
||||
0 is current day, 1 is yesterday etc.
|
||||
Default is 0.
|
||||
-n --filename <file name> The name of the downloaded picture. Defaults to
|
||||
the upstream name.
|
||||
-p --picturedir <picture dir> The full path to the picture download dir.
|
||||
Will be created if it does not exist.
|
||||
[default: $HOME/Pictures/bing-wallpapers/]
|
||||
-r --resolution <resolution> The resolution of the image to retrieve.
|
||||
Supported resolutions: ${RESOLUTIONS[*]}
|
||||
--resolutions <resolutions> The resolutions of the image try to retrieve.
|
||||
eg.: --resolutions "1920x1200 1920x1080 UHD"
|
||||
-m --monitor <num> Set wallpaper only on certain monitor (1,2,3...)
|
||||
--all-desktops-experimental Set wallpaper on all desktops
|
||||
Fixing osascript bug when wallpaper is not set for Desktop 2.
|
||||
Known issue: Minimized apps are removed from Dock.
|
||||
If something goes wrong delete Library/Application Support/Dock/desktoppicture.db
|
||||
and restart your Mac.
|
||||
-h --help Show this screen.
|
||||
--version Show version.
|
||||
EOF
|
||||
}
|
||||
|
||||
create_plist_in_users_agents_folder() {
|
||||
local SCRIPT_PATH=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )/$(basename "${BASH_SOURCE:-$0}")
|
||||
local REST_ARGS=$(echo "$ARGS" | sed -e "s/enable-auto-update//")
|
||||
|
||||
if [ $RUN_USING_NPX ]; then
|
||||
local COMMANDS="<string>source ~/.bashrc && npx --yes bing-wallpaper-daily-mac-multimonitor@latest $REST_ARGS</string>"
|
||||
else
|
||||
local COMMANDS="<string>$SCRIPT_PATH $REST_ARGS</string>"
|
||||
fi
|
||||
|
||||
cat > $PLIST_FILE <<- EOM
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.bing-wallpaper-daily-mac-multimonitor.plist</string>
|
||||
<key>OnDemand</key>
|
||||
<true/>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
$COMMANDS
|
||||
</array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
</dict>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/bing-wallpaper-daily-mac-multimonitor-plist.err</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/bing-wallpaper-daily-mac-multimonitor-plist.out</string>
|
||||
<key>StartInterval</key>
|
||||
<integer>1800</integer>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOM
|
||||
|
||||
launchctl unload -w $PLIST_FILE 2>/dev/null
|
||||
launchctl load -w $PLIST_FILE
|
||||
}
|
||||
|
||||
remove_plist_in_users_agents_folder() {
|
||||
launchctl unload -w "$PLIST_FILE" 2>/dev/null
|
||||
rm "$PLIST_FILE"
|
||||
}
|
||||
|
||||
show_info_text() {
|
||||
# Parse HPImageArchive API and acquire picture BASE URL
|
||||
COPYRIGHT=$(cat "$PICTURE_DIR/info.xml" | \
|
||||
grep -Eo "<copyright>.*<\/copyright>")
|
||||
COPYRIGHT=$(echo "$COPYRIGHT" | sed -e "s/<copyright>//")
|
||||
COPYRIGHT=$(echo "$COPYRIGHT" | sed -e "s/<\/copyright>//")
|
||||
echo $COPYRIGHT
|
||||
}
|
||||
|
||||
print_message() {
|
||||
if [ ! "$QUIET" ]; then
|
||||
printf "%s\n" "$(date): ${1}"
|
||||
fi
|
||||
}
|
||||
|
||||
download_image_curl () {
|
||||
local RES=$1
|
||||
FILEURLWITHRES="${FILEURL}_${RES}.jpg"
|
||||
echo $FILEURLWITHRES
|
||||
FILENAME=${FILEURLWITHRES/\/th\?id=/}
|
||||
FILENAME_LOCAL="${AUTO_UPDATE_NAME}-${FILENAME}"
|
||||
FILEWHOLEURL="$PROTO://bing.com/$FILEURLWITHRES"
|
||||
|
||||
if [ $FORCE ] || [ ! -f "$PICTURE_DIR/$FILENAME_LOCAL" ]; then
|
||||
find $PICTURE_DIR -type f -iname $AUTO_UPDATE_NAME-\*.jpg -delete
|
||||
print_message "Downloading: $FILENAME..."
|
||||
curl --fail -Lo "$PICTURE_DIR/$FILENAME_LOCAL" "$FILEWHOLEURL"
|
||||
curl --fail -Lo "$PICTURE_DIR/info.xml" "$BING_HP_IMAGE_ARCHIVE_URL"
|
||||
if [ "$?" == "0" ]; then
|
||||
FILEPATH="$PICTURE_DIR/$FILENAME_LOCAL"
|
||||
return
|
||||
fi
|
||||
|
||||
FILEPATH=""
|
||||
return
|
||||
else
|
||||
print_message "Skipping download: $FILENAME_LOCAL..."
|
||||
FILEPATH="$PICTURE_DIR/$FILENAME_LOCAL"
|
||||
DOWNLOAD_SKIPPED=true
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
set_wallpaper () {
|
||||
local FILEPATH=$1
|
||||
local MONITOR=$2
|
||||
|
||||
if [ "$MONITOR" -ge 1 ] 2>/dev/null; then
|
||||
print_message "Setting wallpaper for monitor: $MONITOR"
|
||||
osascript - << EOF
|
||||
set tlst to {}
|
||||
tell application "System Events"
|
||||
set tlst to a reference to every desktop
|
||||
set picture of item $MONITOR of tlst to "$FILEPATH"
|
||||
end tell
|
||||
EOF
|
||||
else
|
||||
osascript -e 'tell application "System Events" to tell every desktop to set picture to "'$FILEPATH'"'
|
||||
fi
|
||||
}
|
||||
|
||||
set_wallpaper_experimental () {
|
||||
local db_file="Library/Application Support/Dock/desktoppicture.db"
|
||||
local db_path="$HOME/$db_file"
|
||||
|
||||
# Put the image path in the database
|
||||
local sql="insert into data values(\"$FILEPATH\"); "
|
||||
sqlite3 "$db_path" "$sql"
|
||||
|
||||
# Get the index of the new entry
|
||||
local sql="select max(rowid) from data;"
|
||||
local new_entry=$(sqlite3 "$db_path" "$sql")
|
||||
local new_entry=$(echo $new_entry|tr -d '\n')
|
||||
|
||||
# Get all picture ids (monitor/space pairs)
|
||||
local sql="select rowid from pictures;"
|
||||
local pictures_string=$(sqlite3 "$db_path" "$sql")
|
||||
|
||||
local IFS=$'\n'
|
||||
local pictures=($pictures_string)
|
||||
|
||||
# Clear all existing preferences
|
||||
local sql="select max(rowid) from data; delete from preferences; "
|
||||
|
||||
for pic in "${pictures[@]}"
|
||||
do
|
||||
if [ "$pic" ]; then
|
||||
local sql+="insert into preferences (key, data_id, picture_id) "
|
||||
local sql+="values(1, $new_entry, $pic); "
|
||||
fi
|
||||
done
|
||||
|
||||
sqlite3 "$db_path" "$sql"
|
||||
|
||||
killall "Dock"
|
||||
}
|
||||
|
||||
# Defaults
|
||||
PICTURE_DIR="$HOME/Pictures/bing-wallpapers/"
|
||||
|
||||
# Option parsing
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
enable-auto-update)
|
||||
ENABLE_AUTOMATIC_UPDATE=true
|
||||
;;
|
||||
disable-auto-update)
|
||||
DISABLE_AUTOMATIC_UPDATE=true
|
||||
;;
|
||||
info)
|
||||
INFO=true
|
||||
;;
|
||||
--auto-update-name)
|
||||
AUTO_UPDATE_NAME="$2"
|
||||
shift
|
||||
;;
|
||||
-r|--resolution)
|
||||
RESOLUTION="$2"
|
||||
shift
|
||||
;;
|
||||
-p|--picturedir)
|
||||
PICTURE_DIR="$2"
|
||||
shift
|
||||
;;
|
||||
-c|--country)
|
||||
COUNTRY="$2"
|
||||
shift
|
||||
;;
|
||||
-d|--day)
|
||||
DAY="$2"
|
||||
shift
|
||||
;;
|
||||
-n|--filename)
|
||||
FILENAME="$2"
|
||||
shift
|
||||
;;
|
||||
-m|--monitor)
|
||||
MONITOR="$2"
|
||||
shift
|
||||
;;
|
||||
-f|--force)
|
||||
FORCE=true
|
||||
;;
|
||||
-s|--ssl)
|
||||
SSL=true
|
||||
;;
|
||||
-q|--quiet)
|
||||
QUIET=true
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--resolutions)
|
||||
RESOLUTIONS="$2"
|
||||
shift
|
||||
;;
|
||||
--all-desktops-experimental)
|
||||
EXPERIMENTAL=true
|
||||
;;
|
||||
--version)
|
||||
printf "%s\n" $VERSION
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
(>&2 printf "Unknown parameter: %s\n" "$1")
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Set options
|
||||
[ $QUIET ] && CURL_QUIET='-s'
|
||||
[ $SSL ] && PROTO='https' || PROTO='http'
|
||||
[ $DAY ] && IDX=$DAY || IDX='0'
|
||||
BING_HP_IMAGE_ARCHIVE_URL="https://www.bing.com/HPImageArchive.aspx?format=xml&idx=${IDX}&n=1"
|
||||
[ $COUNTRY ] && BING_HP_IMAGE_ARCHIVE_URL="${BING_HP_IMAGE_ARCHIVE_URL}&mkt=${COUNTRY}"
|
||||
PLIST_FILE="${PLIST_FILE}-${AUTO_UPDATE_NAME}.plist"
|
||||
|
||||
PARENT_COMMAND=$(ps -o comm= $PPID)
|
||||
if [[ "$PARENT_COMMAND" == *"npm exec"* ]]; then
|
||||
RUN_USING_NPX=true
|
||||
fi
|
||||
|
||||
if [ "$ENABLE_AUTOMATIC_UPDATE" ]; then
|
||||
# enable update
|
||||
create_plist_in_users_agents_folder
|
||||
echo "Automatic wallpaper update enabled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$DISABLE_AUTOMATIC_UPDATE" ]; then
|
||||
# disable update
|
||||
remove_plist_in_users_agents_folder
|
||||
echo "Automatic wallpaper update disabled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$INFO" ]; then
|
||||
show_info_text
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create picture directory if it doesn't already exist
|
||||
mkdir -p "${PICTURE_DIR}"
|
||||
|
||||
# Parse HPImageArchive API and acquire picture BASE URL
|
||||
FILEURL=( $(curl -sL $BING_HP_IMAGE_ARCHIVE_URL | \
|
||||
grep -Eo "<urlBase>.*?</urlBase>") )
|
||||
FILEURL=$(echo "$FILEURL" | sed -e "s/<urlBase>//")
|
||||
FILEURL=$(echo "$FILEURL" | sed -e "s/<\/urlBase>//")
|
||||
|
||||
if [ $RESOLUTION ]; then
|
||||
download_image_curl $RESOLUTION
|
||||
if [ "$FILEPATH" ]; then
|
||||
if [ "$EXPERIMENTAL" ]; then
|
||||
if [ ! "$DOWNLOAD_SKIPPED" ]; then
|
||||
set_wallpaper_experimental $FILEPATH
|
||||
fi
|
||||
else
|
||||
set_wallpaper $FILEPATH $MONITOR
|
||||
fi
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for RESOLUTION in "${RESOLUTIONS[@]}"
|
||||
do
|
||||
download_image_curl $RESOLUTION
|
||||
if [ "$FILEPATH" ]; then
|
||||
if [ "$EXPERIMENTAL" ]; then
|
||||
if [ ! "$DOWNLOAD_SKIPPED" ]; then
|
||||
set_wallpaper_experimental $
|
||||
fi
|
||||
else
|
||||
set_wallpaper $FILEPATH $MONITOR
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
19
bin/.local/bin/bootstrap.sh
Executable file
19
bin/.local/bin/bootstrap.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
echo "Linux bootstrapping not implemented yet"
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||
;;
|
||||
Darwin)
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
;;
|
||||
esac
|
||||
brew install stow pass just fzf direnv
|
||||
|
||||
git clone https://code.unbl.ink/secstate/dotfiles ~/.dotfiles
|
||||
(cd ~/.dotfiles && make)
|
||||
git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh ~/.oh-my-zsh
|
||||
source ~/.zshrc
|
||||
scp powellc@192.168.40.208:~/.gnupg ~/.
|
||||
BIN
bin/.local/bin/cgo
Executable file
BIN
bin/.local/bin/cgo
Executable file
Binary file not shown.
65
bin/.local/bin/changepaper
Executable file
65
bin/.local/bin/changepaper
Executable file
@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
# This Script downloads National Geographic Photo of the day, and sets it as desktop background (gnome, unity)
|
||||
# Copyright (C) 2012 Saman Barghi - All Rights Reserved
|
||||
# Permission to copy, modify, and distribute is granted under GPLv3
|
||||
# Last Revised 22 May 2019
|
||||
#######################
|
||||
|
||||
# For feh, we need display set properly
|
||||
export DISPLAY=:0.0
|
||||
export XAUTHORITY=/home/powellc/.Xauthority
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
SOURCE=$1
|
||||
else
|
||||
SOURCE='bing'
|
||||
fi
|
||||
|
||||
# Choices: astrobin,natgeo,nasa,unsplash,bing
|
||||
BASEDIR="$HOME/var/media/backgrounds/$SOURCE"
|
||||
SEARX_BASEDIR="$HOME/var/media/backgrounds/bing"
|
||||
|
||||
# Get daily NatGeo POTD
|
||||
#python3 ~/.bin/get_natgeo_potd.py
|
||||
#python3 ~/.bin/get_astrobin_potd.py
|
||||
#python3 ~/.bin/get_unsplash_potd.py
|
||||
~/.asdf/installs/python/3.11.4/bin/python ~/.bin/get_bing_potd.py
|
||||
|
||||
date=$(date '+%Y-%m-%d')
|
||||
|
||||
#set the current image as wallpaper
|
||||
echo "Setting desktop background"
|
||||
feh --bg-fill $BASEDIR/$date.jpg
|
||||
|
||||
#link slim background to new image
|
||||
#SLIM_BG_FILE=/usr/share/slim/themes/default/background.jpg
|
||||
#echo "Setting Slim background image"
|
||||
#rm $SLIM_BG_FILE
|
||||
#cp $BASEDIR/$date.jpg $SLIM_BG_FILE
|
||||
|
||||
#SEARX_BG_FILE=/usr/local/src/searx/searx/static/themes/oscar/img/bg.jpg
|
||||
#echo "Setting Searx background image"
|
||||
#scp $BASEDIR/$date.jpg search.local:$SEARX_BG_FILE
|
||||
|
||||
# Then grab our APOD image and store it for now
|
||||
#Change directory to where the script resides.
|
||||
#BASEDIR="$HOME/var/inbox/apod_photos"
|
||||
#cd $BASEDIR
|
||||
########################
|
||||
#
|
||||
## Get the APoD image from NASA
|
||||
#img="$(curl https://api.nasa.gov/planetary/apod\?api_key=AdfgdnmmInYgpDMEq3ShMLKjJ7DZ7jyUcgLHWdgw | jq .hdurl | tr -d \")"
|
||||
#
|
||||
##check to see if there is any wallpaper to download
|
||||
#if [ -n "$img" ]
|
||||
#then
|
||||
# img_file=`echo $img | cut -d/ -f 7 | tr -d \"`
|
||||
# curl $img > $img_file
|
||||
# #set the current image as wallpaper
|
||||
# #hsetroot -sane $BASEDIR/$img_file
|
||||
# ##link slim background to new image
|
||||
# #rm /usr/share/slim/themes/default/background.jpg
|
||||
# #ln -s $BASEDIR/$img_file /usr/share/slim/themes/default/background.jpg
|
||||
#else
|
||||
# echo "No Wallpaper today"
|
||||
#fi
|
||||
15
bin/.local/bin/checkmail
Executable file
15
bin/.local/bin/checkmail
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
AFEW_SERVICE="afew"
|
||||
MBSYNC_SERVICE="mbsync"
|
||||
NOTMUCH_SERVICE="notmuch"
|
||||
|
||||
if [ $(pidof $AFEW_SERVICE) ] || [ $(pidof $MBSYNC_SERVICE) ] || [ $(pidof $NOTMUCH_SERVICE) ]; then
|
||||
echo "Checkmail is already running."
|
||||
else
|
||||
echo "Checking for new mail"
|
||||
afew -m -n
|
||||
mbsync -a
|
||||
notmuch new
|
||||
afew -t -n
|
||||
fi
|
||||
exit 0
|
||||
111
bin/.local/bin/checkschoolbus.py
Executable file
111
bin/.local/bin/checkschoolbus.py
Executable file
@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "opencv-python",
|
||||
# "numpy",
|
||||
# "ntfy",
|
||||
# ]
|
||||
# ///
|
||||
import requests
|
||||
import cv2
|
||||
import numpy as np
|
||||
import time
|
||||
|
||||
# URL for the video stream
|
||||
stream_url = 'http://loge.local:8083/stream'
|
||||
public_url = "https://mail.see.unbl.ink/stream"
|
||||
|
||||
# Load a template image of a school bus (you need to have this template)
|
||||
template = cv2.imread('/home/powellc/var/inbox/school_bus_template.jpg', 0)
|
||||
w, h = template.shape[::-1]
|
||||
|
||||
# Open the stream using OpenCV
|
||||
cap = cv2.VideoCapture(stream_url)
|
||||
|
||||
# Check if the stream opened successfully
|
||||
if not cap.isOpened():
|
||||
print("Error: Could not open video stream.")
|
||||
exit()
|
||||
|
||||
# Configure ntfy server and topic
|
||||
ntfy_server = "https://ntfy.unbl.ink" # Replace with your ntfy server
|
||||
topic = "school-bus-alert" # Replace with your desired topic
|
||||
|
||||
# Send notification function with custom server and topic
|
||||
def send_ntfy_notification(title, message):
|
||||
# Construct the full URL with topic (this is how ntfy expects the URL)
|
||||
url = f"{ntfy_server}"
|
||||
|
||||
# Payload for the notification
|
||||
headers = {"Title": title}
|
||||
payload = {
|
||||
"topic": topic,
|
||||
"message": message,
|
||||
}
|
||||
|
||||
# Send the POST request to the ntfy server
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Notification sent successfully.")
|
||||
else:
|
||||
print(f"Failed to send notification: {response.status_code} - {response.text}")
|
||||
|
||||
while True:
|
||||
# Read a frame from the stream
|
||||
ret, frame = cap.read()
|
||||
|
||||
if not ret:
|
||||
print("Failed to capture frame.")
|
||||
continue
|
||||
|
||||
# Convert the frame to grayscale for template matching
|
||||
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Perform template matching on the current frame
|
||||
res = cv2.matchTemplate(gray_frame, template, cv2.TM_CCOEFF_NORMED)
|
||||
threshold = 0.80 # Set your threshold for matching
|
||||
loc = np.where(res >= threshold)
|
||||
|
||||
# If a match is found, send a notification
|
||||
if len(loc[0]) > 0:
|
||||
print("Bus detected!")
|
||||
|
||||
# Save the frame with bounding boxes to a file in /tmp
|
||||
timestamp = time.strftime("%Y%m%d_%H%M")
|
||||
filename = f"school_bus_detection_{timestamp}.jpg"
|
||||
output_path = f"/media/photos/misc/school_bus/{filename}"
|
||||
|
||||
cv2.imwrite(output_path, frame)
|
||||
print(f"Frame with match locations saved to {output_path}")
|
||||
|
||||
# Send notification using ntfy (with both title and message)
|
||||
title = "School Bus Detected!"
|
||||
message = f"Check the video feed at: {public_url}\nMatched image at https://files.lab.unbl.ink/school_bus/{filename}"
|
||||
|
||||
# Send the notification with both title and message
|
||||
send_ntfy_notification(title, message)
|
||||
|
||||
# Draw bounding boxes on the detected matches
|
||||
for pt in zip(*loc[::-1]): # Switch x and y for rectangle drawing
|
||||
cv2.rectangle(frame, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)
|
||||
|
||||
|
||||
print("Pausing for 3 minutes to let the bus leave...")
|
||||
time.sleep(180) # Sleep for 3 minutes (180 seconds)
|
||||
else:
|
||||
print("No bus found, sleeping for 2 seconds")
|
||||
|
||||
# Optional: Display the frame with the match location (for debugging)
|
||||
#for pt in zip(*loc[::-1]):
|
||||
# cv2.rectangle(frame, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)
|
||||
|
||||
## Display the frame (for debugging)
|
||||
#cv2.imshow('Frame', frame)
|
||||
|
||||
time.sleep(2) # two seconds so we don't spam the camera
|
||||
# Break the loop on key press (e.g., 'q' to quit)
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
205
bin/.local/bin/checkworkmail.py
Executable file
205
bin/.local/bin/checkworkmail.py
Executable file
@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
#
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "beautifulsoup4",
|
||||
# "imaplib2",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import imaplib
|
||||
import email
|
||||
from email.header import decode_header
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
|
||||
# ----------------------
|
||||
# CONFIGURATION
|
||||
# ----------------------
|
||||
GMAIL_USER = os.getenv("GMAIL_USER", "your-email@gmail.com")
|
||||
GMAIL_APP_PASSWORD = os.getenv("GMAIL_APP_PASSWORD", "your-app-password")
|
||||
NTFY_TOPIC = os.getenv("NTFY_TOPIC", "KKddGQxVm2LoP0JF")
|
||||
NTFY_SERVER = os.getenv("NTFY_SERVER", "https://ntfy.sh")
|
||||
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "60"))
|
||||
JIRA_SENDERS = ["jira@yourcompany.com", "jira@atlassian.net"]
|
||||
SLACK_SENDER = "notification@slack.com"
|
||||
GITHUB_SENDER = "notifications@github.com"
|
||||
JIRA_BASE_URL = os.getenv("JIRA_BASE_URL", "https://yourcompany.atlassian.net/browse")
|
||||
|
||||
GH_STATE_AND_TITLE_TO_SKIP = ["New Relic Exporter", "cancelled", "skipped", "succeeded"]
|
||||
# ----------------------
|
||||
# HELPERS
|
||||
# ----------------------
|
||||
def sanitize_header(value: str) -> str:
|
||||
return re.sub(r'[\r\n]+', ' ', value).strip()
|
||||
|
||||
def clean_subject(subject):
|
||||
decoded, encoding = decode_header(subject)[0]
|
||||
if isinstance(decoded, bytes):
|
||||
decoded = decoded.decode(encoding or 'utf-8', errors='ignore')
|
||||
return sanitize_header(decoded)
|
||||
|
||||
def extract_body(msg):
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
return part.get_payload(decode=True).decode(errors="ignore")
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/html":
|
||||
html = part.get_payload(decode=True).decode(errors="ignore")
|
||||
return BeautifulSoup(html, "html.parser").get_text()
|
||||
else:
|
||||
payload = msg.get_payload(decode=True).decode(errors="ignore")
|
||||
if msg.get_content_type() == "text/html":
|
||||
return BeautifulSoup(payload, "html.parser").get_text()
|
||||
return payload
|
||||
return "(No readable body content found)"
|
||||
|
||||
def clean_slack_body(body):
|
||||
# Remove intro like: "Hi Colin,\n\nYou have a new direct message from ..."
|
||||
cleaned = re.sub(
|
||||
r"Hi .*?,\s+You have a new direct message from.*?\(.*?\.slack\.com.*?\)\.\s+---",
|
||||
"",
|
||||
body,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return cleaned.strip()
|
||||
|
||||
def clean_slack_body(body):
|
||||
# Remove intro like: "Hi Colin,\n\nYou have a new direct message from ..."
|
||||
cleaned = re.sub(
|
||||
r"Hi .*?,\s+You have a new direct message from.*?\(.*?\.slack\.com.*?\)\.\s+---",
|
||||
"",
|
||||
body,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return cleaned.strip()
|
||||
|
||||
def clean_and_relocate_slack_footer(body):
|
||||
# Match the "View in the archives" block
|
||||
archive_pattern = re.compile(
|
||||
r"(@\w+)?\s*View in the archives: (https://.*?\.slack\.com/\S+)", re.IGNORECASE
|
||||
)
|
||||
|
||||
match = archive_pattern.search(body)
|
||||
if match:
|
||||
slack_url = match.group(2).strip()
|
||||
cleaned_body = archive_pattern.sub("", body).strip()
|
||||
# Format the link cleanly at the bottom
|
||||
footer = f"🔗 View in Slack: {slack_url}"
|
||||
return f"{cleaned_body}\n\n---\n{footer}"
|
||||
else:
|
||||
return body.strip()
|
||||
|
||||
|
||||
def extract_github_link(subject, body):
|
||||
pr_match = re.search(r'\(PR\s+#(\d+)\)', subject)
|
||||
repo_match = re.search(r'\[([^\]]+)\]', subject) # [owner/repo]
|
||||
if pr_match and repo_match:
|
||||
return f"https://github.com/{repo_match.group(1)}/pull/{pr_match.group(1)}"
|
||||
match = re.search(r'https://github\.com/\S+', body)
|
||||
return match.group(0) if match else None
|
||||
|
||||
def extract_jira_link(subject):
|
||||
ticket_match = re.search(r'\b([A-Z]+-\d+)\b', subject)
|
||||
if ticket_match:
|
||||
return f"{JIRA_BASE_URL}/{ticket_match.group(1)}"
|
||||
return None
|
||||
|
||||
def format_message(source, subject, body, link=None):
|
||||
return body.strip() or ""
|
||||
|
||||
def send_ntfy_notification(title, message):
|
||||
url = f"{NTFY_SERVER.rstrip('/')}/{NTFY_TOPIC}"
|
||||
requests.post(url, data=message.encode("utf-8"), headers={"Title": sanitize_header(title)})
|
||||
|
||||
def is_github_email(from_email):
|
||||
return GITHUB_SENDER in from_email.lower()
|
||||
|
||||
def is_jira_email(from_email, subject):
|
||||
return any(s in from_email.lower() for s in JIRA_SENDERS) or re.search(r'[A-Z]+-\d+', subject)
|
||||
|
||||
def is_slack_email(from_email, subject):
|
||||
return SLACK_SENDER in from_email.lower()
|
||||
|
||||
def archive_message(mail, email_id):
|
||||
try:
|
||||
if isinstance(email_id, bytes):
|
||||
email_id = email_id.decode()
|
||||
mail.store(email_id, '+X-GM-LABELS', 'processed')
|
||||
except Exception as e:
|
||||
print(f"Error archiving message {email_id}: {e}")
|
||||
|
||||
# ----------------------
|
||||
# MAIN LOOP
|
||||
# ----------------------
|
||||
def check_notifications():
|
||||
print("Checking inbox...")
|
||||
mail = imaplib.IMAP4_SSL("imap.gmail.com")
|
||||
mail.login(GMAIL_USER, GMAIL_APP_PASSWORD)
|
||||
mail.select("inbox")
|
||||
|
||||
#result, data = mail.search(None, '(UNSEEN)')
|
||||
ok, data = mail.search(None, 'UNSEEN X-GM-LABELS', "inbox")
|
||||
|
||||
#search_criteria = r'(UNSEEN X-GM-LABELS "\\Inbox")'
|
||||
#result, data = mail.uid('search', None, search_criteria)
|
||||
|
||||
mail_ids = data[0].split()
|
||||
|
||||
if not mail_ids:
|
||||
print("No new notifications.")
|
||||
mail.logout()
|
||||
return
|
||||
|
||||
for num in mail_ids:
|
||||
result, msg_data = mail.fetch(num, '(RFC822)')
|
||||
raw_email = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
|
||||
subject = clean_subject(msg.get("Subject", "No Subject"))
|
||||
from_email = msg.get("From", "")
|
||||
body = extract_body(msg)
|
||||
|
||||
if is_github_email(from_email):
|
||||
source = "GitHub"
|
||||
link = extract_github_link(subject, body)
|
||||
elif is_jira_email(from_email, subject):
|
||||
source = "Jira"
|
||||
link = extract_jira_link(subject)
|
||||
elif is_slack_email(from_email, subject):
|
||||
source = "Slack"
|
||||
link = ""
|
||||
body = clean_and_relocate_slack_footer(clean_slack_body(body))
|
||||
else:
|
||||
print(f"Skipping non-matching email: {subject}")
|
||||
continue
|
||||
|
||||
message = format_message(source, subject, body, link)
|
||||
send_notice = True
|
||||
for exclusion in GH_STATE_AND_TITLE_TO_SKIP:
|
||||
if exclusion in subject:
|
||||
print(f"Skipping notification: {subject}")
|
||||
send_notice = False
|
||||
if send_notice:
|
||||
print(f"Sending {source} notification: {subject}")
|
||||
send_ntfy_notification(subject, message)
|
||||
archive_message(mail, num)
|
||||
|
||||
mail.expunge()
|
||||
mail.logout()
|
||||
|
||||
# ----------------------
|
||||
# RUNNER
|
||||
# ----------------------
|
||||
if __name__ == "__main__":
|
||||
while True:
|
||||
try:
|
||||
check_notifications()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
105
bin/.local/bin/create-emacsclient-app-macos.sh
Executable file
105
bin/.local/bin/create-emacsclient-app-macos.sh
Executable file
@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create Emacsclient.app for macOS
|
||||
# This script creates a proper .app bundle that uses emacsclient
|
||||
|
||||
APP_NAME="Emacsclient"
|
||||
APP_DIR="/Applications/${APP_NAME}.app"
|
||||
CONTENTS_DIR="${APP_DIR}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
|
||||
# Adjust these paths based on your system
|
||||
EMACS_BIN="/usr/local/bin/emacs"
|
||||
EMACSCLIENT_BIN="/usr/local/bin/emacsclient"
|
||||
|
||||
# For Apple Silicon Macs with Homebrew, use:
|
||||
# EMACS_BIN="/opt/homebrew/bin/emacs"
|
||||
# EMACSCLIENT_BIN="/opt/homebrew/bin/emacsclient"
|
||||
|
||||
echo "Creating ${APP_NAME}.app..."
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "${MACOS_DIR}"
|
||||
mkdir -p "${RESOURCES_DIR}"
|
||||
|
||||
# Create the launcher script
|
||||
cat > "${MACOS_DIR}/${APP_NAME}" << EOF
|
||||
#!/bin/bash
|
||||
|
||||
# Check if Emacs server is running
|
||||
if ! ${EMACSCLIENT_BIN} -e "(boundp 'server-process)" 2>/dev/null | grep -q "t"; then
|
||||
# Start Emacs daemon if not running
|
||||
${EMACS_BIN} --daemon
|
||||
# Wait a moment for daemon to start
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Open emacsclient with a new frame
|
||||
# -c creates a new frame
|
||||
# -n returns immediately without waiting
|
||||
exec ${EMACSCLIENT_BIN} -c -n "\$@"
|
||||
EOF
|
||||
|
||||
# Make the launcher executable
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME}"
|
||||
|
||||
# Create Info.plist
|
||||
cat > "${CONTENTS_DIR}/Info.plist" << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Emacsclient</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Emacs.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.gnu.Emacsclient</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Emacsclient</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>*</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>All Files</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Copy icon from existing Emacs.app if available
|
||||
if [ -f "/Applications/Emacs.app/Contents/Resources/Emacs.icns" ]; then
|
||||
cp "/Applications/Emacs.app/Contents/Resources/Emacs.icns" "${RESOURCES_DIR}/"
|
||||
echo "Icon copied from Emacs.app"
|
||||
elif [ -f "/opt/homebrew/Caskroom/emacs/*/Emacs.app/Contents/Resources/Emacs.icns" ]; then
|
||||
cp /opt/homebrew/Caskroom/emacs/*/Emacs.app/Contents/Resources/Emacs.icns "${RESOURCES_DIR}/"
|
||||
echo "Icon copied from Homebrew Emacs.app"
|
||||
else
|
||||
echo "Warning: Could not find Emacs.icns icon file"
|
||||
fi
|
||||
|
||||
echo "Successfully created ${APP_DIR}"
|
||||
echo ""
|
||||
echo "You can now:"
|
||||
echo "1. Double-click Emacsclient in /Applications to launch"
|
||||
echo "2. Add it to your Dock"
|
||||
echo "3. Set it as default handler for text files"
|
||||
echo "4. Open files with: open -a Emacsclient file.txt"
|
||||
2
bin/.local/bin/doom
Executable file
2
bin/.local/bin/doom
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
~/.emacs.d/bin/doom $1
|
||||
BIN
bin/.local/bin/ec
Executable file
BIN
bin/.local/bin/ec
Executable file
Binary file not shown.
22
bin/.local/bin/fade
Executable file
22
bin/.local/bin/fade
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
max_brightness=$(cat /sys/class/backlight/intel_backlight/max_brightness)
|
||||
brightness=$(cat /sys/class/backlight/intel_backlight/brightness)
|
||||
step=$((max_brightness / 12))
|
||||
inc=2
|
||||
low_delta=$((brightness - step))
|
||||
high_delta=$((brightness + step))
|
||||
|
||||
if { [ "$1" = "down" ] && [ $low_delta -ge $step ]; } || { [ "$1" = "up" ] && [ $high_delta -le "$max_brightness" ]; }; then
|
||||
for i in $(seq $step); do
|
||||
case $1 in
|
||||
'up')
|
||||
brightnessctl s +$inc 1>/dev/null
|
||||
;;
|
||||
'down')
|
||||
brightnessctl s $inc- 1>/dev/null
|
||||
;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
echo "Brightness too high or low"
|
||||
fi
|
||||
37
bin/.local/bin/get_astrobin_potd.py
Executable file
37
bin/.local/bin/get_astrobin_potd.py
Executable file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
home = os.path.expanduser("~")
|
||||
target_path = f"{home}/var/media/backgrounds/astrobin/{today}.jpg"
|
||||
|
||||
# If the file for today already exists, just exit
|
||||
if os.path.isfile(target_path):
|
||||
print(f"Astrobin image for {today} already exists, skipping download")
|
||||
exit()
|
||||
|
||||
root = "https://www.astrobin.com"
|
||||
api_key = "3f542cbb23407bde6f20490f377366582dd1a54c"
|
||||
api_secret = (
|
||||
subprocess.check_output("pass personal/apikey/astrobin", shell=True)
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
fmt = "json"
|
||||
iotd_uri = f"{root}/api/v1/imageoftheday/?limit=1&api_key={api_key}&api_secret={api_secret}&format={fmt}"
|
||||
r = requests.get(iotd_uri)
|
||||
image_info_uri = r.json()["objects"][0]["image"]
|
||||
r = requests.get(
|
||||
f"{root}{image_info_uri}?api_key={api_key}&api_secret={api_secret}&format={fmt}"
|
||||
)
|
||||
image_uri = r.json()["url_real"]
|
||||
img = requests.get(image_uri, stream=True)
|
||||
|
||||
handle = open(target_path, "wb")
|
||||
for chunk in img.iter_content(chunk_size=512):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
handle.write(chunk)
|
||||
44
bin/.local/bin/get_bing_potd.py
Executable file
44
bin/.local/bin/get_bing_potd.py
Executable file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from PIL import Image
|
||||
|
||||
import imagehash
|
||||
import requests
|
||||
|
||||
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
home = os.path.expanduser("~")
|
||||
check_path = f"{home}/var/media/backgrounds/bing/{yesterday}.jpg"
|
||||
target_path = f"{home}/var/media/backgrounds/bing/{today}.jpg"
|
||||
|
||||
# If the file for today already exists, just exit
|
||||
if os.path.isfile(target_path):
|
||||
|
||||
found_match = False
|
||||
try:
|
||||
yesterday_img = imagehash.average_hash(Image.open(check_path))
|
||||
today_img = imagehash.average_hash(Image.open(target_path))
|
||||
cutoff = 5 # maximum bits that could be different between the hashes.
|
||||
|
||||
found_match = today_img - yesterday_img < cutoff
|
||||
except:
|
||||
pass
|
||||
|
||||
if not found_match:
|
||||
print(f"Bing image for {today} already exists, skipping download")
|
||||
exit()
|
||||
|
||||
print(f"Bing image for {today} same as yesterday, overwriting")
|
||||
|
||||
iotd_uri = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=1&n=1"
|
||||
r = requests.get(iotd_uri)
|
||||
image_info_uri = r.json()["images"][0]["url"]
|
||||
img = requests.get(f"https://www.bing.com{image_info_uri}", stream=True)
|
||||
|
||||
handle = open(target_path, "wb")
|
||||
for chunk in img.iter_content(chunk_size=512):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
handle.write(chunk)
|
||||
34
bin/.local/bin/get_natgeo_potd.py
Executable file
34
bin/.local/bin/get_natgeo_potd.py
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
home = os.path.expanduser("~")
|
||||
target_path = f"{home}/var/media/backgrounds/natgeo/{today}.jpg"
|
||||
|
||||
# If the file for today already exists, just exit
|
||||
if os.path.isfile(target_path):
|
||||
print(f"NatGeo image for {today} already exists, skipping download")
|
||||
exit()
|
||||
|
||||
try:
|
||||
# We've got to go rummaging for:
|
||||
# <meta name="twitter:image:src" content="<full_image_path">
|
||||
potd_uri = "https://www.nationalgeographic.com/photography/photo-of-the-day/"
|
||||
html_doc = requests.get(potd_uri).content
|
||||
soup = BeautifulSoup(html_doc, "html.parser")
|
||||
|
||||
twitter_image_tag = soup.select('meta[name="twitter:image:src"]')[0]
|
||||
|
||||
image_tag = twitter_image_tag
|
||||
img = requests.get(image_tag.attrs["content"], stream=True)
|
||||
|
||||
handle = open(target_path, "wb")
|
||||
for chunk in img.iter_content(chunk_size=512):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
handle.write(chunk)
|
||||
except: # noqa
|
||||
print("No new NatGeo image for today, sorry.")
|
||||
24
bin/.local/bin/get_unsplash_potd.py
Executable file
24
bin/.local/bin/get_unsplash_potd.py
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
home = os.path.expanduser("~")
|
||||
target_path = f"{home}/var/media/backgrounds/unsplash/{today}.jpg"
|
||||
|
||||
# If the file for today already exists, just exit
|
||||
if os.path.isfile(target_path):
|
||||
print(f"Unsplash image for {today} already exists, skipping download")
|
||||
exit()
|
||||
|
||||
root = "https://source.unsplash.com"
|
||||
iotd_uri = f"{root}/random"
|
||||
img = requests.get(iotd_uri, stream=True)
|
||||
|
||||
handle = open(target_path, "wb")
|
||||
for chunk in img.iter_content(chunk_size=512):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
handle.write(chunk)
|
||||
66
bin/.local/bin/gnome-changepaper
Executable file
66
bin/.local/bin/gnome-changepaper
Executable file
@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
# This Script downloads National Geographic Photo of the day, and sets it as desktop background (gnome, unity)
|
||||
# Copyright (C) 2012 Saman Barghi - All Rights Reserved
|
||||
# Permission to copy, modify, and distribute is granted under GPLv3
|
||||
# Last Revised 22 May 2019
|
||||
#######################
|
||||
|
||||
# For feh, we need display set properly
|
||||
export DISPLAY=:0.0
|
||||
export XAUTHORITY=/home/powellc/.Xauthority
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
SOURCE=$1
|
||||
else
|
||||
SOURCE='bing'
|
||||
fi
|
||||
|
||||
# Choices: astrobin,natgeo,nasa,unsplash,bing
|
||||
BASEDIR="$HOME/var/media/backgrounds/$SOURCE"
|
||||
SEARX_BASEDIR="$HOME/var/media/backgrounds/bing"
|
||||
|
||||
# Get daily NatGeo POTD
|
||||
#python3 ~/.bin/get_natgeo_potd.py
|
||||
#python3 ~/.bin/get_astrobin_potd.py
|
||||
#python3 ~/.bin/get_unsplash_potd.py
|
||||
~/.asdf/installs/python/3.11.4/bin/python ~/.bin/get_bing_potd.py
|
||||
|
||||
date=$(date '+%Y-%m-%d')
|
||||
|
||||
#set the current image as wallpaper
|
||||
echo "Setting desktop background"
|
||||
gsettings set org.gnome.desktop.background picture-uri $BASEDIR/$date.jpg
|
||||
gsettings set org.gnome.desktop.background picture-uri-dark $BASEDIR/$date.jpg
|
||||
|
||||
#link slim background to new image
|
||||
#SLIM_BG_FILE=/usr/share/slim/themes/default/background.jpg
|
||||
#echo "Setting Slim background image"
|
||||
#rm $SLIM_BG_FILE
|
||||
#cp $BASEDIR/$date.jpg $SLIM_BG_FILE
|
||||
|
||||
#SEARX_BG_FILE=/usr/local/src/searx/searx/static/themes/oscar/img/bg.jpg
|
||||
#echo "Setting Searx background image"
|
||||
#scp $BASEDIR/$date.jpg search.local:$SEARX_BG_FILE
|
||||
|
||||
# Then grab our APOD image and store it for now
|
||||
#Change directory to where the script resides.
|
||||
#BASEDIR="$HOME/var/inbox/apod_photos"
|
||||
#cd $BASEDIR
|
||||
########################
|
||||
#
|
||||
## Get the APoD image from NASA
|
||||
#img="$(curl https://api.nasa.gov/planetary/apod\?api_key=AdfgdnmmInYgpDMEq3ShMLKjJ7DZ7jyUcgLHWdgw | jq .hdurl | tr -d \")"
|
||||
#
|
||||
##check to see if there is any wallpaper to download
|
||||
#if [ -n "$img" ]
|
||||
#then
|
||||
# img_file=`echo $img | cut -d/ -f 7 | tr -d \"`
|
||||
# curl $img > $img_file
|
||||
# #set the current image as wallpaper
|
||||
# #hsetroot -sane $BASEDIR/$img_file
|
||||
# ##link slim background to new image
|
||||
# #rm /usr/share/slim/themes/default/background.jpg
|
||||
# #ln -s $BASEDIR/$img_file /usr/share/slim/themes/default/background.jpg
|
||||
#else
|
||||
# echo "No Wallpaper today"
|
||||
#fi
|
||||
14
bin/.local/bin/gnome-i3-keybindings.sh
Executable file
14
bin/.local/bin/gnome-i3-keybindings.sh
Executable file
@ -0,0 +1,14 @@
|
||||
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-1 "['<alt>1']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-2 "['<alt>2']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-3 "['<alt>3']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-4 "['<alt>4']"
|
||||
|
||||
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-1 "['<alt><shift>1']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-2 "['<alt><shift>2']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-3 "['<alt><shift>3']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-4 "['<alt><shift>4']"
|
||||
|
||||
gsettings set org.gnome.desktop.wm.keybindings switch-windows "['<alt>j']"
|
||||
gsettings set org.gnome.desktop.wm.keybindings switch-windows-backward "['<alt>k']"
|
||||
|
||||
gsettings set org.gnome.desktop.wm.keybindings close "['<alt><shift>c']"
|
||||
7
bin/.local/bin/hrvpn
Executable file
7
bin/.local/bin/hrvpn
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
start) openvpn3 session-start --config ~/var/inbox/hungryroot.ovpn ;;
|
||||
stop) openvpn3 session-manage --config ~/var/inbox/hungryroot.ovpn --disconnect ;;
|
||||
status) openvpn3 sessions-list ;;
|
||||
esac
|
||||
|
||||
253
bin/.local/bin/hungryroot-notifications
Executable file
253
bin/.local/bin/hungryroot-notifications
Executable file
@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "requests>=2.31.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import email
|
||||
import imaplib
|
||||
import os
|
||||
import ssl
|
||||
import time
|
||||
import re
|
||||
from email.header import decode_header, make_header
|
||||
|
||||
import requests
|
||||
|
||||
FORWARDED_HEADER_RE = re.compile(
|
||||
r"^(From:.*?\n(?:.*\n)*?\n)",
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
PREFIX_RE = re.compile(r"^\s*(?:(?:re|fw|fwd)\s*:\s*)+", re.IGNORECASE)
|
||||
HUNGRYROOT_REPOS_RE = re.compile(r"^\s*(?:\[hungryroot/[^\]]+\]\s*)+", re.IGNORECASE)
|
||||
|
||||
GITHUB_URL_RE = re.compile(r"https://github\.com/[^\s>,]+")
|
||||
|
||||
FOOTER_START_MARKERS = (
|
||||
"reply to this email directly",
|
||||
"you are receiving this because",
|
||||
)
|
||||
|
||||
IMAP_HOST = os.environ.get("IMAP_HOST", "box.unbl.ink")
|
||||
IMAP_USER = os.environ.get("IMAP_USER", "hungryroot@unbl.ink")
|
||||
IMAP_PASS = os.environ.get("IMAP_PASS", "")
|
||||
|
||||
NTFY_BASE = os.environ.get("NTFY_BASE", "https://ntfy.unbl.ink")
|
||||
NTFY_TOPIC = os.environ.get("NTFY_TOPIC", "hroot-notifications")
|
||||
NTFY_ERROR_TOPIC = os.environ.get("NTFY_ERROR_TOPIC", "errors")
|
||||
POLL_SECONDS = int(os.environ.get("POLL_SECONDS", "20"))
|
||||
|
||||
# Optional: if your ntfy needs auth
|
||||
NTFY_USER = os.environ.get("NTFY_USER")
|
||||
NTFY_PASS = os.environ.get("NTFY_PASS")
|
||||
NTFY_TOKEN = os.environ.get("NTFY_TOKEN") # alternative to user/pass
|
||||
|
||||
MAX_BODY_CHARS = int(os.environ.get("MAX_BODY_CHARS", "500"))
|
||||
|
||||
|
||||
def _decode(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
try:
|
||||
return str(make_header(decode_header(value)))
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
|
||||
def _extract_text(msg: email.message.Message) -> str:
|
||||
"""Best-effort plain text extraction."""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
ctype = part.get_content_type()
|
||||
disp = (part.get("Content-Disposition") or "").lower()
|
||||
if ctype == "text/plain" and "attachment" not in disp:
|
||||
payload = part.get_payload(decode=True) or b""
|
||||
charset = part.get_content_charset() or "utf-8"
|
||||
return payload.decode(charset, errors="replace")
|
||||
return ""
|
||||
payload = msg.get_payload(decode=True) or b""
|
||||
charset = msg.get_content_charset() or "utf-8"
|
||||
return payload.decode(charset, errors="replace")
|
||||
|
||||
|
||||
def _first_github_link(text: str) -> str | None:
|
||||
# crude but effective; improve later if you want
|
||||
for token in text.split():
|
||||
if token.startswith("https://github.com/"):
|
||||
return token.strip(")>.,;\"'")
|
||||
return None
|
||||
|
||||
def _sanitize_header(value: str) -> str:
|
||||
# Collapse CR/LF and excessive whitespace into single spaces
|
||||
return " ".join(value.replace("\r", " ").replace("\n", " ").split())
|
||||
|
||||
def normalize_title(title: str) -> str:
|
||||
# Unfold/sanitize any CRLF + weird spacing from forwarded headers
|
||||
title = " ".join(title.replace("\r", " ").replace("\n", " ").split())
|
||||
|
||||
# Strip Re:/Fwd: chains like "Re: Re: Fwd:"
|
||||
title = PREFIX_RE.sub("", title).strip()
|
||||
|
||||
# Strip ONLY hungryroot repo prefix blocks, keep [BE-####] etc.
|
||||
title = HUNGRYROOT_REPOS_RE.sub("", title).strip()
|
||||
|
||||
return title
|
||||
|
||||
def strip_github_footer(text: str) -> tuple[str, str | None]:
|
||||
"""
|
||||
Strip GitHub's email footer and return (clean_text, github_url).
|
||||
"""
|
||||
lines = text.splitlines()
|
||||
clean_lines = []
|
||||
github_url = None
|
||||
in_footer = False
|
||||
|
||||
for line in lines:
|
||||
lower = line.lower()
|
||||
|
||||
if not in_footer and any(m in lower for m in FOOTER_START_MARKERS):
|
||||
in_footer = True
|
||||
continue
|
||||
|
||||
if in_footer:
|
||||
if github_url is None:
|
||||
match = GITHUB_URL_RE.search(line)
|
||||
if match:
|
||||
github_url = match.group(0)
|
||||
continue
|
||||
|
||||
clean_lines.append(line)
|
||||
|
||||
clean_text = "\n".join(clean_lines).rstrip()
|
||||
return clean_text, github_url
|
||||
|
||||
|
||||
def strip_forwarded_headers(text: str) -> str:
|
||||
"""
|
||||
Strip a forwarded-message header block, including folded headers.
|
||||
"""
|
||||
lines = text.splitlines()
|
||||
out = []
|
||||
in_headers = True
|
||||
|
||||
for line in lines:
|
||||
if in_headers:
|
||||
# End of headers = first completely empty line
|
||||
if line.strip() == "":
|
||||
in_headers = False
|
||||
# Still in headers: skip
|
||||
continue
|
||||
out.append(line)
|
||||
|
||||
return "\n".join(out).lstrip()
|
||||
|
||||
def notify_ntfy(title: str, message: str, url: str | None = None):
|
||||
endpoint = f"{NTFY_BASE.rstrip('/')}/{NTFY_TOPIC}"
|
||||
|
||||
safe_title = normalize_title(title)
|
||||
|
||||
headers = {
|
||||
"Title": safe_title,
|
||||
"Tags": "github",
|
||||
"Priority": "4",
|
||||
}
|
||||
if url:
|
||||
headers["Click"] = url
|
||||
|
||||
auth = None
|
||||
if NTFY_TOKEN:
|
||||
headers["Authorization"] = f"Bearer {NTFY_TOKEN}"
|
||||
elif NTFY_USER and NTFY_PASS:
|
||||
auth = (NTFY_USER, NTFY_PASS)
|
||||
|
||||
r = requests.post(
|
||||
endpoint,
|
||||
data=message.encode("utf-8"),
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=10,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
def notify_error(err: str):
|
||||
endpoint = f"{NTFY_BASE.rstrip('/')}/{NTFY_ERROR_TOPIC}"
|
||||
headers = {
|
||||
"Title": "hungryroot-notifications error",
|
||||
"Tags": "warning",
|
||||
"Priority": "5",
|
||||
}
|
||||
|
||||
auth = None
|
||||
if NTFY_TOKEN:
|
||||
headers["Authorization"] = f"Bearer {NTFY_TOKEN}"
|
||||
elif NTFY_USER and NTFY_PASS:
|
||||
auth = (NTFY_USER, NTFY_PASS)
|
||||
|
||||
# Keep it short so it doesn't spam
|
||||
body = err.strip()
|
||||
if len(body) > 1500:
|
||||
body = body[:1500] + "…"
|
||||
|
||||
r = requests.post(endpoint, data=body.encode("utf-8"), headers=headers, auth=auth, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
|
||||
def fetch_unseen():
|
||||
context = ssl.create_default_context()
|
||||
with imaplib.IMAP4_SSL(IMAP_HOST, ssl_context=context) as M:
|
||||
M.login(IMAP_USER, IMAP_PASS)
|
||||
M.select("INBOX")
|
||||
|
||||
typ, data = M.search(None, "UNSEEN")
|
||||
if typ != "OK":
|
||||
return []
|
||||
|
||||
uids = data[0].split()
|
||||
out = []
|
||||
|
||||
for uid in uids:
|
||||
typ, msg_data = M.fetch(uid, "(RFC822)")
|
||||
if typ != "OK" or not msg_data or not msg_data[0]:
|
||||
continue
|
||||
|
||||
raw = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw)
|
||||
|
||||
subject = _decode(msg.get("Subject", "GitHub notification"))
|
||||
from_ = _decode(msg.get("From", ""))
|
||||
|
||||
|
||||
body = strip_forwarded_headers(_extract_text(msg))
|
||||
body, github_url = strip_github_footer(body)
|
||||
|
||||
out.append((uid, subject, from_, body, github_url))
|
||||
|
||||
# mark seen so we don't resend
|
||||
M.store(uid, "+FLAGS", "\\Seen")
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
try:
|
||||
items = fetch_unseen()
|
||||
for _uid, subject, from_, snippet, github_url in items:
|
||||
title = subject
|
||||
msg = f"{snippet}\n\nFrom: {from_}"
|
||||
notify_ntfy(title=title, message=msg, url=github_url)
|
||||
except Exception as e:
|
||||
msg = f"[hungryroot-notifications] {type(e).__name__}: {e}"
|
||||
print(msg)
|
||||
try:
|
||||
notify_error(msg)
|
||||
except Exception as e2:
|
||||
print(f"[hungryroot-notifications] failed to notify error: {e2}")
|
||||
time.sleep(POLL_SECONDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
14
bin/.local/bin/hungryroot-notifications-wrapper
Executable file
14
bin/.local/bin/hungryroot-notifications-wrapper
Executable file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Load secret from pass
|
||||
export IMAP_PASS="$(pass work/hungryroot@unbl.ink)"
|
||||
|
||||
# Optional sanity check
|
||||
if [[ -z "${IMAP_PASS}" ]]; then
|
||||
echo "IMAP_PASS is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$HOME/.bin/hungryroot-notifications"
|
||||
|
||||
491
bin/.local/bin/imapdedup
Executable file
491
bin/.local/bin/imapdedup
Executable file
@ -0,0 +1,491 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# imapdedup.py
|
||||
#
|
||||
# Looks for duplicate messages in a set of IMAP mailboxes and removes all but the first.
|
||||
# Comparison is normally based on the Message-ID header.
|
||||
#
|
||||
# Default behaviour is purely to mark the duplicates as deleted. Some mail clients
|
||||
# will allow you to view these and undelete them if you change your mind.
|
||||
#
|
||||
# Copyright (c) 2013-2020 Quentin Stafford-Fraser.
|
||||
# All rights reserved, subject to the following:
|
||||
#
|
||||
#
|
||||
# This is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
||||
# USA.
|
||||
#
|
||||
|
||||
import getpass
|
||||
import hashlib
|
||||
import imaplib
|
||||
import os
|
||||
import optparse
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
from typing import List, Dict, Tuple, Optional, Union, Type, Any
|
||||
|
||||
from email.parser import BytesParser
|
||||
from email.message import Message
|
||||
from email.errors import HeaderParseError
|
||||
from email.header import decode_header
|
||||
|
||||
# Increase the rather small limit on result line-length
|
||||
# imposed in certain imaplib versions.
|
||||
# imaplib._MAXLINE = max(2000000, imaplib._MAXLINE)
|
||||
|
||||
|
||||
class ImapDedupException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# IMAP responses should normally begin 'OK' - we strip that off
|
||||
def check_response(resp: Tuple[str, List[bytes]]):
|
||||
status, value = resp
|
||||
if status != "OK":
|
||||
raise ImapDedupException("Got response: %s from server" % str(value))
|
||||
return value
|
||||
|
||||
|
||||
def get_arguments(args: List[str]) -> Tuple[optparse.Values, List[str]]:
|
||||
# Get arguments and create link to server
|
||||
|
||||
parser = optparse.OptionParser(usage="%prog [options] <mailboxname> [<mailboxname> ...]")
|
||||
parser.add_option(
|
||||
"-P", "--process", dest="process", help="IMAP process to access mailboxes"
|
||||
)
|
||||
parser.add_option("-s", "--server", dest="server", help="IMAP server")
|
||||
parser.add_option("-p", "--port", dest="port", help="IMAP server port", type="int")
|
||||
parser.add_option("-x", "--ssl", dest="ssl", action="store_true", help="Use SSL")
|
||||
parser.add_option("-X", "--starttls", dest="starttls", action="store_true", help="Require STARTTLS")
|
||||
parser.add_option("-u", "--user", dest="user", help="IMAP user name")
|
||||
parser.add_option(
|
||||
"-w",
|
||||
"--password",
|
||||
dest="password",
|
||||
help="IMAP password (Will prompt if not specified)",
|
||||
)
|
||||
parser.add_option(
|
||||
"-v", "--verbose", dest="verbose", action="store_true", help="Verbose mode"
|
||||
)
|
||||
parser.add_option(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
dest="dry_run",
|
||||
action="store_true",
|
||||
help="Don't actually do anything, just report what would be done",
|
||||
)
|
||||
parser.add_option(
|
||||
"-c",
|
||||
"--checksum",
|
||||
dest="use_checksum",
|
||||
action="store_true",
|
||||
help="Use a checksum of several mail headers, instead of the Message-ID",
|
||||
)
|
||||
parser.add_option(
|
||||
"-m",
|
||||
"--checksum-with-id",
|
||||
dest="use_id_in_checksum",
|
||||
action="store_true",
|
||||
help="Include the Message-ID (if any) in the -c checksum.",
|
||||
)
|
||||
parser.add_option(
|
||||
"",
|
||||
"--no-close",
|
||||
dest="no_close",
|
||||
action="store_true",
|
||||
help='Do not "close" mailbox when done. Some servers will purge deleted messages on a close command.',
|
||||
)
|
||||
parser.add_option(
|
||||
"-l",
|
||||
"--list",
|
||||
dest="just_list",
|
||||
action="store_true",
|
||||
help="Just list mailboxes",
|
||||
)
|
||||
|
||||
parser.set_defaults(
|
||||
verbose=False, ssl=False, dry_run=False, no_close=False, just_list=False
|
||||
)
|
||||
(options, mboxes) = parser.parse_args(args)
|
||||
if ((not options.server) or (not options.user)) and not options.process:
|
||||
sys.stderr.write(
|
||||
"\nError: Must specify server, user, and at least one mailbox.\n\n"
|
||||
)
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
if not options.password and not options.process:
|
||||
# Read from IMAPDEDUP_PASSWORD env variable, or prompt for one.
|
||||
options.password = os.getenv("IMAPDEDUP_PASSWORD") or getpass.getpass()
|
||||
|
||||
if options.use_id_in_checksum and not options.use_checksum:
|
||||
sys.stderr.write("\nError: If you use -m you must also use -c.\n")
|
||||
sys.exit(1)
|
||||
|
||||
return (options, mboxes)
|
||||
|
||||
|
||||
# Thanks to http://www.doughellmann.com/PyMOTW/imaplib/
|
||||
list_response_pattern = re.compile(
|
||||
rb'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)'
|
||||
)
|
||||
|
||||
|
||||
def parse_list_response(line: bytes):
|
||||
m = list_response_pattern.match(line)
|
||||
if m is None:
|
||||
sys.stderr.write("\nError: parsing list response '{}'".format(str(line)))
|
||||
sys.exit(1)
|
||||
flags, delimiter, mailbox_name = m.groups()
|
||||
mailbox_name = mailbox_name.strip(b'"')
|
||||
return (flags, delimiter, mailbox_name)
|
||||
|
||||
|
||||
def str_header(parsed_message: Message, name: str) -> str:
|
||||
""""
|
||||
Return the value (of the first instance, if more than one) of
|
||||
the given header, as a unicode string.
|
||||
"""
|
||||
hdrlist = decode_header(parsed_message.get(name, ""))
|
||||
btext, charset = hdrlist[0]
|
||||
if isinstance(btext, str):
|
||||
text = btext
|
||||
else:
|
||||
text = btext.decode("utf-8", "ignore")
|
||||
return text
|
||||
|
||||
|
||||
def get_message_id(
|
||||
parsed_message: Message, options_use_checksum=False, options_use_id_in_checksum=False
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Normally, return the Message-ID header (or print a warning if it doesn't
|
||||
exist and return None).
|
||||
|
||||
If options_use_checksum is specified, use md5 hash of several headers
|
||||
instead.
|
||||
|
||||
For more safety, user should first do a dry run, reviewing them before
|
||||
deletion. Problems are extremely unlikely, but md5 is not collision-free.
|
||||
|
||||
If options_use_id_in_checksum is specified, then the Message-ID will be
|
||||
included in the header checksum, otherwise it is excluded.
|
||||
"""
|
||||
try:
|
||||
if options_use_checksum:
|
||||
md5 = hashlib.md5()
|
||||
md5.update(("From:" + str_header(parsed_message, "From")).encode())
|
||||
md5.update(("To:" + str_header(parsed_message, "To")).encode())
|
||||
md5.update(("Subject:" + str_header(parsed_message, "Subject")).encode())
|
||||
md5.update(("Date:" + str_header(parsed_message, "Date")).encode())
|
||||
md5.update(("Cc:" + str_header(parsed_message, "Cc")).encode())
|
||||
md5.update(("Bcc:" + str_header(parsed_message, "Bcc")).encode())
|
||||
if options_use_id_in_checksum:
|
||||
md5.update(("Message-ID:" + str_header(parsed_message, "Message-ID")).encode())
|
||||
msg_id = md5.hexdigest()
|
||||
# print(msg_id)
|
||||
else:
|
||||
msg_id = str_header(parsed_message, "Message-ID")
|
||||
if not msg_id:
|
||||
print(
|
||||
(
|
||||
"Message '%s' dated '%s' has no Message-ID header."
|
||||
% (
|
||||
str_header(parsed_message, "Subject"),
|
||||
str_header(parsed_message, "Date"),
|
||||
)
|
||||
)
|
||||
)
|
||||
print("You might want to use the -c option.")
|
||||
return None
|
||||
return msg_id
|
||||
except (ValueError, HeaderParseError):
|
||||
print(
|
||||
"WARNING: There was an exception trying to parse the headers of this message."
|
||||
)
|
||||
print("It may be corrupt, and you might consider deleting it.")
|
||||
print(
|
||||
(
|
||||
"Subject: %s\nFrom: %s\nDate: %s\n"
|
||||
% (
|
||||
parsed_message["Subject"],
|
||||
parsed_message["From"],
|
||||
parsed_message["Date"],
|
||||
)
|
||||
)
|
||||
)
|
||||
print("Message skipped.")
|
||||
return None
|
||||
|
||||
|
||||
def get_mailbox_list(server: imaplib.IMAP4) -> List[str]:
|
||||
"""
|
||||
Return a list of usable mailbox names
|
||||
"""
|
||||
resp = []
|
||||
for mb in check_response(server.list()):
|
||||
bits = parse_list_response(mb)
|
||||
if rb"\\Noselect" not in bits[0]:
|
||||
resp.append(bits[2].decode())
|
||||
return resp
|
||||
|
||||
def get_deleted_msgnums(server: imaplib.IMAP4) -> List[int]:
|
||||
"""
|
||||
Return a list of ids of deleted messages in the folder.
|
||||
"""
|
||||
resp = []
|
||||
deleted_info = check_response(server.search(None, "DELETED"))
|
||||
if deleted_info:
|
||||
# If neither None nor empty, then
|
||||
# the first item should be a list of msg ids
|
||||
resp = [int(n) for n in deleted_info[0].split()]
|
||||
return resp
|
||||
|
||||
def get_undeleted_msgnums(server: imaplib.IMAP4) -> List[int]:
|
||||
"""
|
||||
Return a list of ids of non-deleted messages in the folder.
|
||||
"""
|
||||
resp = []
|
||||
undeleted_info = check_response(server.search(None, "UNDELETED"))
|
||||
if undeleted_info:
|
||||
# If neither None nor empty, then
|
||||
# the first item should be a list of msg ids
|
||||
resp = [int(n) for n in undeleted_info[0].split()]
|
||||
return resp
|
||||
|
||||
|
||||
def mark_messages_deleted(server: imaplib.IMAP4, msgs_to_delete: List[int]):
|
||||
message_ids = ",".join(map(str, msgs_to_delete))
|
||||
check_response(
|
||||
server.store(message_ids, "+FLAGS", r"(\Deleted)")
|
||||
)
|
||||
|
||||
def get_msg_headers(server: imaplib.IMAP4, msg_ids: List[int]) -> List[Tuple[int, bytes]]:
|
||||
"""
|
||||
Get the dict of headers for each message in the list of provided IDs.
|
||||
Return a list of tuples: [ (msgid, header_bytes), (msgid, header_bytes)... ]
|
||||
The returned header_bytes can be parsed by
|
||||
"""
|
||||
# Get the header info for each message
|
||||
message_ids_str = ",".join(map(str, msg_ids))
|
||||
ms = check_response(server.fetch(message_ids_str, "(RFC822.HEADER)"))
|
||||
|
||||
# There are two lines per message in the response
|
||||
resp: List[Tuple[int, bytes]] = []
|
||||
for ci in range(0, len(ms) // 2):
|
||||
mnum = int(msg_ids[ci])
|
||||
_, hinfo = ms[ci * 2]
|
||||
resp.append((mnum, hinfo))
|
||||
return resp
|
||||
|
||||
|
||||
def print_message_info(parsed_message: Message):
|
||||
print("From: " + str_header(parsed_message, "From"))
|
||||
print("To: " + str_header(parsed_message, "To"))
|
||||
print("Cc: " + str_header(parsed_message, "Cc"))
|
||||
print("Bcc: " + str_header(parsed_message, "Bcc"))
|
||||
print("Subject: " + str_header(parsed_message, "Subject"))
|
||||
print("Date: " + str_header(parsed_message, "Date"))
|
||||
print("")
|
||||
|
||||
|
||||
# This actually does the work
|
||||
def process(options, mboxes: List[str]):
|
||||
serverclass: Type[Any]
|
||||
if options.process:
|
||||
serverclass = imaplib.IMAP4_stream
|
||||
elif options.ssl:
|
||||
serverclass = imaplib.IMAP4_SSL
|
||||
else:
|
||||
serverclass = imaplib.IMAP4
|
||||
|
||||
try:
|
||||
if options.process:
|
||||
server = serverclass(options.process)
|
||||
elif options.port:
|
||||
server = serverclass(options.server, options.port)
|
||||
else:
|
||||
# Use the default, which will be different depending on SSL choice
|
||||
server = serverclass(options.server)
|
||||
except socket.error as e:
|
||||
sys.stderr.write(
|
||||
"\nFailed to connect to server. Might be host, port or SSL settings?\n"
|
||||
)
|
||||
sys.stderr.write("%s\n\n" % e)
|
||||
sys.exit(1)
|
||||
|
||||
if ("STARTTLS" in server.capabilities) and hasattr(server, "starttls"):
|
||||
server.starttls()
|
||||
elif options.starttls:
|
||||
sys.stderr.write("\nError: Server did not offer TLS\n")
|
||||
sys.exit(1)
|
||||
elif not options.ssl:
|
||||
sys.stderr.write("\nWarning: Unencrypted connection\n")
|
||||
|
||||
try:
|
||||
if not options.process:
|
||||
server.login(options.user, options.password)
|
||||
except:
|
||||
sys.stderr.write("\nError: Login failed\n")
|
||||
sys.exit(1)
|
||||
|
||||
# List mailboxes option
|
||||
# Just do that and then exit
|
||||
if options.just_list:
|
||||
for mb in get_mailbox_list(server):
|
||||
print(mb)
|
||||
return
|
||||
|
||||
if len(mboxes) == 0:
|
||||
sys.stderr.write("\nError: Must specify mailbox\n")
|
||||
sys.exit(1)
|
||||
|
||||
# OK - let's get started.
|
||||
# Iterate through a set of named mailboxes and delete the later messages discovered.
|
||||
try:
|
||||
parser = BytesParser() # can be the same for all mailboxes
|
||||
# Create a list of previously seen message IDs, in any mailbox
|
||||
msg_ids: Dict[str, str] = {}
|
||||
for mbox in mboxes:
|
||||
msgs_to_delete = [] # should be reset for each mbox
|
||||
msg_map = {} # should be reset for each mbox
|
||||
|
||||
# Make sure mailbox name is surrounded by quotes if it contains a space
|
||||
if " " in mbox and (mbox[0] != '"' or mbox[-1] != '"'):
|
||||
mbox = '"' + mbox + '"'
|
||||
|
||||
# Select the mailbox
|
||||
msgs = check_response(server.select(mailbox=mbox, readonly=options.dry_run))[0]
|
||||
print("There are %d messages in %s." % (int(msgs), mbox))
|
||||
|
||||
# Check how many messages are already marked 'deleted'...
|
||||
numdeleted = len(get_deleted_msgnums(server))
|
||||
print(
|
||||
"%s message(s) currently marked as deleted in %s"
|
||||
% (numdeleted or "No", mbox)
|
||||
)
|
||||
|
||||
# Now get a list of the ones that aren't deleted.
|
||||
# That's what we'll actually use.
|
||||
msgnums = get_undeleted_msgnums(server)
|
||||
print("%s others in %s" % (len(msgnums), mbox))
|
||||
|
||||
chunkSize = 100
|
||||
if options.verbose:
|
||||
print("Reading the others... (in batches of %d)" % chunkSize)
|
||||
|
||||
for i in range(0, len(msgnums), chunkSize):
|
||||
if options.verbose:
|
||||
print("Batch starting at item %d" % i)
|
||||
|
||||
# and parse them.
|
||||
for mnum, hinfo in get_msg_headers(server, msgnums[i: i + chunkSize]):
|
||||
# Parse the header info into a Message object
|
||||
mp = parser.parsebytes(hinfo)
|
||||
|
||||
if options.verbose:
|
||||
print("Checking %s message %s" % (mbox, mnum))
|
||||
# Store message only when verbose is enabled (to print it later on)
|
||||
msg_map[mnum] = mp
|
||||
|
||||
# Record the message-ID header (or generate one from other headers)
|
||||
msg_id = get_message_id(
|
||||
mp, options.use_checksum, options.use_id_in_checksum
|
||||
)
|
||||
|
||||
if msg_id:
|
||||
# If we've seen this message before, record it as one to be
|
||||
# deleted in this mailbox.
|
||||
if msg_id in msg_ids:
|
||||
print(
|
||||
"Message %s_%s is a duplicate of %s and %s be marked as deleted"
|
||||
% (
|
||||
mbox, mnum, msg_ids[msg_id],
|
||||
options.dry_run and "would" or "will",
|
||||
)
|
||||
)
|
||||
if options.verbose:
|
||||
print(
|
||||
"Subject: %s\nFrom: %s\nDate: %s\n"
|
||||
% (mp["Subject"], mp["From"], mp["Date"])
|
||||
)
|
||||
msgs_to_delete.append(mnum)
|
||||
# Otherwise just record the fact that we've seen it
|
||||
else:
|
||||
msg_ids[msg_id] = f"{mbox}_{mnum}"
|
||||
|
||||
print(
|
||||
(
|
||||
"%s message(s) in %s processed"
|
||||
% (min(len(msgnums), i + chunkSize), mbox)
|
||||
)
|
||||
)
|
||||
|
||||
# OK - we've been through this mailbox, and msgs_to_delete holds
|
||||
# a list of the duplicates we've found.
|
||||
|
||||
if len(msgs_to_delete) == 0:
|
||||
print("No duplicates were found in %s" % mbox)
|
||||
|
||||
else:
|
||||
if options.verbose:
|
||||
print("These are the duplicate messages: ")
|
||||
for mnum in msgs_to_delete:
|
||||
print_message_info(msg_map[mnum])
|
||||
|
||||
if options.dry_run:
|
||||
print(
|
||||
"If you had NOT selected the 'dry-run' option,\n"
|
||||
" %i messages would now be marked as 'deleted'."
|
||||
% (len(msgs_to_delete))
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
print("Marking %i messages as deleted..." % (len(msgs_to_delete)))
|
||||
# Deleting messages one at a time can be slow if there are many,
|
||||
# so we batch them up.
|
||||
chunkSize = 30
|
||||
if options.verbose:
|
||||
print("(in batches of %d)" % chunkSize)
|
||||
for i in range(0, len(msgs_to_delete), chunkSize):
|
||||
mark_messages_deleted(server, msgs_to_delete[i: i + chunkSize])
|
||||
if options.verbose:
|
||||
print("Batch starting at item %d marked." % i)
|
||||
print("Confirming new numbers...")
|
||||
numdeleted = len(get_deleted_msgnums(server))
|
||||
numundel = len(get_undeleted_msgnums(server))
|
||||
print(
|
||||
"There are now %s messages marked as deleted and %s others in %s."
|
||||
% (numdeleted, numundel, mbox)
|
||||
)
|
||||
|
||||
if not options.no_close:
|
||||
server.close()
|
||||
|
||||
except ImapDedupException as e:
|
||||
print("Error:", e, file=sys.stderr)
|
||||
finally:
|
||||
server.logout()
|
||||
|
||||
|
||||
def main(args: List[str]):
|
||||
options, mboxes = get_arguments(args)
|
||||
process(options, mboxes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
BIN
bin/.local/bin/jira
Executable file
BIN
bin/.local/bin/jira
Executable file
Binary file not shown.
3
bin/.local/bin/kitty
Executable file
3
bin/.local/bin/kitty
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
export KITTY_DISABLE_WAYLAND=1
|
||||
exec /usr/bin/kitty
|
||||
42
bin/.local/bin/load_keys
Executable file
42
bin/.local/bin/load_keys
Executable file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${HOST:-$(hostname -s 2>/dev/null || hostname)}"
|
||||
PASS_BASE="personal/ssh"
|
||||
STORE_ROOT="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
|
||||
ABS_BASE_PATH="${STORE_ROOT}/${PASS_BASE}"
|
||||
|
||||
# Ensure ssh-agent is running
|
||||
if [[ -z "${SSH_AUTH_SOCK:-}" ]]; then
|
||||
eval "$(ssh-agent -s)"
|
||||
fi
|
||||
|
||||
# Verify the base path exists
|
||||
if [[ ! -d "$ABS_BASE_PATH" ]]; then
|
||||
echo "ERROR: Base path not found in pass: $PASS_BASE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Loop through each identity subdirectory
|
||||
echo -n "Loading ssh keys for host: "
|
||||
while IFS= read -r dir; do
|
||||
IDENTITY=$(basename "$dir")
|
||||
|
||||
# Find the latest .gpg file by name (ISO sort) and hostname
|
||||
LATEST_FILE=$(find "$dir" -maxdepth 1 -name "*.gpg" -exec basename {} \; \
|
||||
| sed 's/\.gpg$//' \
|
||||
| sort -r \
|
||||
| head -n 1)
|
||||
|
||||
if [[ -z "$LATEST_FILE" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -n "$HOST"
|
||||
|
||||
# Decrypt and pipe directly to ssh-add
|
||||
# The '-' tells ssh-add to read the key from standard input (stdin)
|
||||
pass show "${PASS_BASE}/${IDENTITY}/${LATEST_FILE}" | ssh-add - >/dev/null 2>&1
|
||||
|
||||
done < <(find "$ABS_BASE_PATH" -mindepth 1 -maxdepth 1 -type d -name "*${HOST}*")
|
||||
73
bin/.local/bin/mail-ntfy.py
Executable file
73
bin/.local/bin/mail-ntfy.py
Executable file
@ -0,0 +1,73 @@
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import requests
|
||||
import os
|
||||
import imaplib
|
||||
import email
|
||||
from email.header import decode_header
|
||||
|
||||
user = os.environ.get("MAIL_USER")
|
||||
password = os.environ.get("MAIL_PASS")
|
||||
|
||||
N = 15
|
||||
|
||||
def send_ntfy():
|
||||
print("Checking for new email ... ")
|
||||
if not password:
|
||||
print("Please set your password in the MAIL_PASS environment variable")
|
||||
return
|
||||
|
||||
if not user:
|
||||
print("Please set your user in the MAIL_USER environment variable")
|
||||
return
|
||||
|
||||
conn = imaplib.IMAP4_SSL("box.unbl.ink")
|
||||
conn.login(user, password)
|
||||
|
||||
status, messages = conn.select("INBOX")
|
||||
status, unread = conn.search(None, "UnSeen")
|
||||
|
||||
|
||||
try:
|
||||
unread = unread[0].decode()
|
||||
except IndexError:
|
||||
print("No new messages found")
|
||||
unread_list = unread.split(" ")
|
||||
if unread_list == ['']:
|
||||
print("No new messages found")
|
||||
return
|
||||
|
||||
print(f"Found {len(unread_list)} unread emails")
|
||||
if unread_list:
|
||||
for msg_num in unread_list:
|
||||
# fetch the email message by ID
|
||||
res, msg = conn.fetch(msg_num, "(RFC822)")
|
||||
for response in msg:
|
||||
if isinstance(response, tuple):
|
||||
msg = email.message_from_bytes(response[1])
|
||||
subject, encoding = decode_header(msg["Subject"])[0]
|
||||
if isinstance(subject, bytes):
|
||||
subject = subject.decode(encoding)
|
||||
From, encoding = decode_header(msg.get("From"))[0]
|
||||
if isinstance(From, bytes):
|
||||
From = From.decode(encoding)
|
||||
print(f"Found {subject}")
|
||||
requests.post(
|
||||
"https://ntfy.unbl.ink/207-comms",
|
||||
data=subject.encode("utf-8"),
|
||||
headers={
|
||||
"Title": From,
|
||||
"Tags": "envelope",
|
||||
"Click": "https://box.unbl.ink/mail/?_task=mail&_mbox=INBOX"
|
||||
}
|
||||
)
|
||||
conn.close()
|
||||
conn.logout()
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_ntfy()
|
||||
|
||||
18
bin/.local/bin/newjail
Executable file
18
bin/.local/bin/newjail
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# Create the jail with dhcp on
|
||||
iocage create -r 14.1-RELEASE --name $1 dhcp=on boot=on
|
||||
|
||||
# Install mdnsd and .local DNS
|
||||
iocage exec -f $1 "pkg install -y python311 openmdns && sysrc mdnsd_enable="YES" && sysrc mdnsd_flags=epair0b && service mdnsd start"
|
||||
|
||||
# Copy our jail public key to allow login
|
||||
mkdir /tank/iocage/jails/$1/root/root/.ssh
|
||||
cp /home/powellc/.ssh/jails.pub /tank/iocage/jails/$1/root/root/.ssh/authorized_keys
|
||||
|
||||
# Allow root login and start SSH
|
||||
iocage exec -f $1 "echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && sysrc sshd_enable="YES" && service sshd start"
|
||||
|
||||
# Add our new IP address to our unbound local zone
|
||||
#ip_address=$(iocage exec $1 ifconfig epair0b | grep 'inet ' | awk '{print $2}')
|
||||
#echo 'local-data: "'$1'.service IN A '$ip_address'"' >> /tank/iocage/jails/dns/root/var/unbound/service.zones
|
||||
#iocage exec dns service unbound restart
|
||||
19
bin/.local/bin/notify-on-new-mail.py
Executable file
19
bin/.local/bin/notify-on-new-mail.py
Executable file
@ -0,0 +1,19 @@
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
new_mail_list = json.loads(
|
||||
subprocess.check_output(
|
||||
["notmuch", "show", "--format=json", "tag:inbox"],
|
||||
).decode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
if new_mail_list:
|
||||
output = "Mail's here!\n\n"
|
||||
for message in new_mail_list:
|
||||
msg = message[0][0]
|
||||
dt = msg['headers']['Date']
|
||||
subj = msg['headers']['Subject']
|
||||
output += f"{dt}\n{subj}\n\n"
|
||||
|
||||
subprocess.run(["dunstify", output])
|
||||
17
bin/.local/bin/nyt
Executable file
17
bin/.local/bin/nyt
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
BASEDIR="/media/photos/misc/nytimes"
|
||||
DATE=$(date +"%Y-%m-%d")
|
||||
TMPFILE=/tmp/nyt.pdf
|
||||
OUTFILE=$BASEDIR/$DATE.jpg
|
||||
|
||||
if test -f "$OUTFILE"; then
|
||||
echo "NYT front page already found. Not downloading."
|
||||
else
|
||||
DATEPATH=$(date +"%Y/%m/%d")
|
||||
curl -o /tmp/nyt.pdf -L https://static01.nyt.com/images/$DATEPATH/nytfrontpage/scan.pdf
|
||||
convert -density 150 -quality 75 $TMPFILE $OUTFILE
|
||||
rm $BASEDIR/today.jpg
|
||||
cp $OUTFILE $BASEDIR/today.jpg
|
||||
curl -H "X-Title: NYT headlines today" -d "https://files.lab.unbl.ink/nytimes/today.jpg" https://ntfy.unbl.ink/news
|
||||
rm $TMPFILE
|
||||
fi
|
||||
33
bin/.local/bin/onsubnet
Executable file
33
bin/.local/bin/onsubnet
Executable file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]] || [[ "$1" == "" ]] ; then
|
||||
printf "Usage:\n\tonsubnet [ --not ] partial-ip-address\n\n"
|
||||
printf "Example:\n\tonsubnet 10.10.\n\tonsubnet --not 192.168.0.\n\n"
|
||||
printf "Note:\n\tThe partial-ip-address must match starting at the first\n"
|
||||
printf "\tcharacter of the ip-address, therefore the first example\n"
|
||||
printf "\tabove will match 10.10.10.1 but not 110.10.10.1\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
on=0
|
||||
off=1
|
||||
if [[ "$1" == "--not" ]] ; then
|
||||
shift
|
||||
on=1
|
||||
off=0
|
||||
fi
|
||||
|
||||
regexp="^$(sed 's/\./\\./g' <<<"$1")"
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]] ; then
|
||||
ifconfig | grep -F 'inet ' | grep -Fv 127.0.0. | cut -d ' ' -f 2 | grep $1
|
||||
else
|
||||
hostname -I | tr -s " " "\012" | grep -Fv 127.0.0. | grep $1
|
||||
fi
|
||||
|
||||
if [[ $? == 0 ]]; then
|
||||
exit $on
|
||||
else
|
||||
exit $off
|
||||
fi
|
||||
|
||||
29
bin/.local/bin/pinentry-wrapper
Executable file
29
bin/.local/bin/pinentry-wrapper
Executable file
@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
# Deterministic, context-aware pinentry selector
|
||||
|
||||
# 1. Emacs — only when actually inside Emacs
|
||||
if [ -n "$INSIDE_EMACS" ] && command -v pinentry-emacs >/dev/null 2>&1; then
|
||||
exec pinentry-emacs "$@"
|
||||
fi
|
||||
|
||||
# 2. macOS GUI
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
if [ -x /opt/homebrew/bin/pinentry-mac ]; then exec /opt/homebrew/bin/pinentry-mac "$@"; fi
|
||||
if [ -x /usr/local/bin/pinentry-mac ]; then exec /usr/local/bin/pinentry-mac "$@"; fi
|
||||
fi
|
||||
|
||||
# 3. Linux GUI — only if a GUI session exists
|
||||
if [ -n "$WAYLAND_DISPLAY" ] || [ -n "$DISPLAY" ]; then
|
||||
if command -v pinentry-gnome3 >/dev/null 2>&1; then exec pinentry-gnome3 "$@"; fi
|
||||
if command -v pinentry-gtk-2 >/dev/null 2>&1; then exec pinentry-gtk-2 "$@"; fi
|
||||
if command -v pinentry-qt >/dev/null 2>&1; then exec pinentry-qt "$@"; fi
|
||||
fi
|
||||
|
||||
# 4. TTY fallback
|
||||
if command -v pinentry-curses >/dev/null 2>&1; then
|
||||
exec pinentry-curses "$@"
|
||||
fi
|
||||
|
||||
# 5. Last resort
|
||||
exec pinentry "$@"
|
||||
|
||||
56
bin/.local/bin/record
Executable file
56
bin/.local/bin/record
Executable file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import wave
|
||||
|
||||
import pyaudio
|
||||
|
||||
|
||||
def record_audio(seconds: int, filename: str) -> None:
|
||||
oformat = pyaudio.paInt16
|
||||
channels = 2
|
||||
rate = 44100
|
||||
chunk = 1024
|
||||
record_seconds = int(seconds)
|
||||
wave_output_filename = f"{filename}.wav"
|
||||
|
||||
audio = pyaudio.PyAudio()
|
||||
|
||||
# start Recording
|
||||
stream = audio.open(
|
||||
format=oformat,
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
frames_per_buffer=chunk,
|
||||
)
|
||||
print(f"recording for {seconds} seconds ...")
|
||||
frames = []
|
||||
for i in range(0, int(rate / chunk * record_seconds)):
|
||||
data = stream.read(chunk)
|
||||
frames.append(data)
|
||||
|
||||
s = i / (rate / chunk)
|
||||
if s % 1 < 0.025:
|
||||
if int(seconds - s) < 11:
|
||||
print(int(seconds - s) + 1, ' sec left')
|
||||
|
||||
print(f"recording saved to {wave_output_filename}")
|
||||
|
||||
# stop Recording
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
audio.terminate()
|
||||
|
||||
waveFile = wave.open(wave_output_filename, "wb")
|
||||
waveFile.setnchannels(channels)
|
||||
waveFile.setsampwidth(audio.get_sample_size(oformat))
|
||||
waveFile.setframerate(rate)
|
||||
waveFile.writeframes(b"".join(frames))
|
||||
waveFile.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv
|
||||
sec = args[1] if args[1:] else 15
|
||||
name = args[2] if args[2:] else 'recording'
|
||||
record_audio(int(sec), name)
|
||||
70
bin/.local/bin/rekey
Executable file
70
bin/.local/bin/rekey
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
USER="${USER:-$(whoami)}"
|
||||
HOST="${HOST:-$(hostname -s 2>/dev/null || hostname)}"
|
||||
DATE="$(date +%F)"
|
||||
SSH_DIR="$HOME/.ssh"
|
||||
mkdir -p "$SSH_DIR"
|
||||
PASS_PATH="personal/ssh/$USER@$HOST/$DATE"
|
||||
|
||||
DOTFILES_DIR="$HOME/.dotfiles"
|
||||
COMMIT_MSG="[ssh] Rekey host $HOST"
|
||||
|
||||
# --- Temporary directory for private key (macOS compatible) ---
|
||||
# Tries /dev/shm if present (Linux), otherwise falls back to standard temp.
|
||||
if [[ -d /dev/shm && -w /dev/shm ]]; then
|
||||
TMPDIR_BASE="/dev/shm"
|
||||
else
|
||||
TMPDIR_BASE="${TMPDIR:-/tmp}"
|
||||
fi
|
||||
|
||||
TMP_WORKDIR="$(mktemp -d "$TMPDIR_BASE/sshkey.${USER}.${HOST}.${DATE}.XXXXXX")"
|
||||
TMP_PRIV="$TMP_WORKDIR/id_ed25519"
|
||||
|
||||
cleanup() {
|
||||
# best-effort secure cleanup: delete key material and remove temp dir
|
||||
rm -f "$TMP_PRIV" "$TMP_PRIV.pub" 2>/dev/null || true
|
||||
rmdir "$TMP_WORKDIR" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Generate Ed25519 key pair into temp dir
|
||||
ssh-keygen -t ed25519 -f "$TMP_PRIV" -N "" -q
|
||||
|
||||
# Insert private key into pass
|
||||
pass insert --multiline --force "$PASS_PATH" < "$TMP_PRIV"
|
||||
echo "Private key stored in pass at $PASS_PATH"
|
||||
|
||||
# Extract public key from the same temp file
|
||||
ssh-keygen -y -f "$TMP_PRIV" > "$SSH_DIR/$USER@$HOST.pub"
|
||||
chmod 600 "$SSH_DIR/$USER@$HOST.pub" 2>/dev/null || true
|
||||
echo "Public key written to $SSH_DIR/$USER@$HOST.pub"
|
||||
|
||||
# Temp key removed automatically by trap
|
||||
cat "$SSH_DIR/$USER@$HOST.pub" >> ~/.ssh/authorized_keys
|
||||
echo "Public key added to authorized_keys"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Git commit dotfiles changes (ssh + password-store)
|
||||
# ------------------------------------------------------------------
|
||||
COMMIT_MSG="[ssh] Rekey host $HOST"
|
||||
|
||||
if [[ -d "$DOTFILES_DIR/.git" ]]; then
|
||||
pushd "$DOTFILES_DIR" >/dev/null
|
||||
|
||||
# Stage only relevant dirs
|
||||
git add ssh password-store 2>/dev/null || true
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "$COMMIT_MSG"
|
||||
git push
|
||||
echo "Dotfiles committed & pushed: $COMMIT_MSG"
|
||||
else
|
||||
echo "No dotfiles changes to commit"
|
||||
fi
|
||||
|
||||
popd >/dev/null
|
||||
else
|
||||
echo "Dotfiles repo not found at $DOTFILES_DIR — skipping commit"
|
||||
fi
|
||||
3
bin/.local/bin/run_snapclient
Executable file
3
bin/.local/bin/run_snapclient
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
snapclient -h snapcast.service &
|
||||
8
bin/.local/bin/screencap
Executable file
8
bin/.local/bin/screencap
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env sh
|
||||
# FFmppeg screen capture
|
||||
#
|
||||
REC_iface=$(pactl list sources short | awk '{print$2}' | grep 'monitor')
|
||||
SCREEN_res=$(xrandr -q --current | grep '*' | awk '{print$1}')
|
||||
|
||||
#ffmpeg -f x11grab -r 25 -s 1920x1080 -i :0.0 -vcodec libx264 -f alsa -ac 2 -i pulse -acodec aac output.mkv
|
||||
ffmpeg -video_size 1920x1080 -framerate 25 -f x11grab -i :0.0 -f alsa -ac 2 -i pulse ~/tmp/output.mp4
|
||||
682
bin/.local/bin/slack
Executable file
682
bin/.local/bin/slack
Executable file
@ -0,0 +1,682 @@
|
||||
#!/usr/bin/env bash
|
||||
export SLACK_CLI_TOKEN=$(pass personal/slack/token)
|
||||
|
||||
bindir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
etcdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [ -n "${SLACK_CLI_TOKEN}" ]; then
|
||||
token="${SLACK_CLI_TOKEN}"
|
||||
elif [ -r "${etcdir}/.slack" ] && [ -f "${etcdir}/.slack" ]; then
|
||||
token=$(sed -n '1p' < "${etcdir}/.slack")
|
||||
elif [ -r "${HOME}/.slack" ] && [ -f "${HOME}/.slack" ]; then
|
||||
token=$(sed -n '1p' < "${HOME}/.slack")
|
||||
elif [ -r "/etc/.slack" ] && [ -f "/etc/.slack" ]; then
|
||||
token=$(sed -n '1p' < "/etc/.slack")
|
||||
fi
|
||||
|
||||
# COMMAND PARSING #################################################################################
|
||||
cmd="${1}" ; shift
|
||||
|
||||
# SUBCOMMAND PARSING ##############################################################################
|
||||
case "${cmd}${1}" in
|
||||
chatdelete|chatsend|chatupdate|\
|
||||
filedelete|fileinfo|filelist|fileupload|\
|
||||
presenceactive|presenceaway|\
|
||||
reminderadd|remindercomplete|reminderdelete|reminderinfo|reminderlist| \
|
||||
snoozeend|snoozeinfo|snoozestart|\
|
||||
statusclear|statusedit)
|
||||
sub=${1} ; shift
|
||||
;;
|
||||
esac
|
||||
|
||||
# PIPE FRIENDLINESS ###############################################################################
|
||||
case "${cmd}${sub}" in
|
||||
chatsend|chatupdate|fileupload) [ -p /dev/stdin ] && stdin=$(cat <&0) ;;
|
||||
esac
|
||||
|
||||
# ARGUMENT AND OPTION PARSING #####################################################################
|
||||
while (( "$#" )); do
|
||||
case "${1}" in
|
||||
--actions=*) actions=${1/--actions=/''} ; shift ;;
|
||||
--actions*|-a*) actions=${2} ; shift ; shift ;;
|
||||
--author-icon=*) authoricon=${1/--author-icon=/''} ; shift ;;
|
||||
--author-icon*|-ai*) authoricon=${2} ; shift ; shift ;;
|
||||
--author-link=*) authorlink=${1/--author-link=/''} ; shift ;;
|
||||
--author-link*|-al*) titlelink=${2} ; shift ; shift ;;
|
||||
--author=*) author=${1/--author=/''} ; shift ;;
|
||||
--author*|-at*) author=${2} ; shift ; shift ;;
|
||||
--channels=*) channels=${1/--channels=/''} ; shift ;;
|
||||
--channels*|-chs*) channels=${2} ; shift ; shift ;;
|
||||
--channel=*) channel=${1/--channel=/''} ; shift ;;
|
||||
--channel*|-ch*) channel=${2} ; shift ; shift ;;
|
||||
--color=*) color=${1/--color=/''} ; shift ;;
|
||||
--color*|-cl*) color=${2} ; shift ; shift ;;
|
||||
--compact|-cp) compact='-c' ; shift ;;
|
||||
--comment=*) comment=${1/--comment=/''} ; shift ;;
|
||||
--comment*|-cm*) comment=${2} ; shift ; shift ;;
|
||||
--count=*) count=${1/--count=/''} ; shift ;;
|
||||
--count|-cn*) count=${2} ; shift ; shift ;;
|
||||
--emoji=*) emoji=${1/--emoji=/''} ; shift ;;
|
||||
--emoji*|-em*) emoji=${2} ; shift ; shift ;;
|
||||
--fields=*) fields=${1/--fields=/''} ; shift ;;
|
||||
--fields*|-flds*) fields=${2} ; shift ; shift ;;
|
||||
--filename=*) filename=${1/--filename=/''} ; shift ;;
|
||||
--filename*|-fln*) filename=${2} ; shift ; shift ;;
|
||||
--filetype=*) filetype=${1/--filetype=/''} ; shift ;;
|
||||
--filetype*|-flt*) filetype=${2} ; shift ; shift ;;
|
||||
--file=*) file=${1/--file=/''} ; shift ;;
|
||||
--file*|-fl*) file=${2} ; shift ; shift ;;
|
||||
--filter=*) filter=${1/--filter=/''} ; shift ;;
|
||||
--filter*|-f*) filter=${2} ; shift ; shift ;;
|
||||
--footer-icon=*) footericon=${1/--footer-icon=/''} ; shift ;;
|
||||
--footer-icon*|-fi*) footericon=${2} ; shift ; shift ;;
|
||||
--footer=*) footer=${1/--footer=/''} ; shift ;;
|
||||
--footer*|-ft*) footer=${2} ; shift ; shift ;;
|
||||
--image=*) image=${1/--image-url=/''} ; shift ;;
|
||||
--image*|-im*) image=${2} ; shift ; shift ;;
|
||||
--monochrome|-m) monochrome='-M' ; shift ;;
|
||||
--minutes=*) minutes=${1/--minutes=/''} ; shift ;;
|
||||
--minutes*|-mn*) minutes=${2} ; shift ; shift ;;
|
||||
--page=*) page=${1/--page=/''} ; shift ;;
|
||||
--page|-pg*) page=${2} ; shift ; shift ;;
|
||||
--pretext=*) pretext=${1/--pretext=/''} ; shift ;;
|
||||
--pretext*|-pt*) pretext=${2} ; shift ; shift ;;
|
||||
--reminder=*) reminder=${1/--reminder=/''} ; shift ;;
|
||||
--reminder|-rm*) reminder=${2} ; shift ; shift ;;
|
||||
--text=*) text=${1/--text=/''} ; shift ;;
|
||||
--text*|-tx*) text=${2} ; shift ; shift ;;
|
||||
--thumbnail=*) thumbnail=${1/--thumbnail=/''} ; shift ;;
|
||||
--thumbnail*|-th*) thumbnail=${2} ; shift ; shift ;;
|
||||
--time=*) _time=${1/--time=/''} ; shift ;;
|
||||
--time|-tm*) _time=${2} ; shift ; shift ;;
|
||||
--timestamp-from=*) timestampfrom=${1/--timestampfrom=/''} ; shift ;;
|
||||
--timestamp-from|-tf*) timestampfrom=${2} ; shift ; shift ;;
|
||||
--timestamp-to=*) timestampto=${1/--timestampto=/''} ; shift ;;
|
||||
--timestamp-to|-tt*) timestampto=${2} ; shift ; shift ;;
|
||||
--timestamp=*) timestamp=${1/--timestamp=/''} ; shift ;;
|
||||
--timestamp*|-ts*) timestamp=${2} ; shift ; shift ;;
|
||||
--title-link=*) titlelink=${1/--title-link=/''} ; shift ;;
|
||||
--title-link*|-tl*) titlelink=${2} ; shift ; shift ;;
|
||||
--title=*) title=${1/--title=/''} ; shift ;;
|
||||
--title*|-ti*) title=${2} ; shift ; shift ;;
|
||||
--token=*) token=${1/--token=/''} ; shift ;;
|
||||
--token|-tk*) token=${2} ; shift ; shift ;;
|
||||
--trace|-x) trace='set -x' ; shift ;;
|
||||
--types=*) types=${1/--types=/''} ; shift ;;
|
||||
--types|-ty*) types=${2} ; shift ; shift ;;
|
||||
--user=*) user=${1/--user=/''} ; shift ;;
|
||||
--user|-ur*) user=${2} ; shift ; shift ;;
|
||||
*)
|
||||
case "${cmd}${sub}" in
|
||||
chatdelete)
|
||||
[ -n "${1}" ] && [ -n "${timestamp}" ] && [ -z "${channel}" ] && channel=${1}
|
||||
[ -n "${1}" ] && [ -z "${timestamp}" ] && timestamp=${1}
|
||||
;;
|
||||
chatsend)
|
||||
[ -n "${1}" ] && [ -n "${text}" ] && [ -z "${channel}" ] && channel=${1}
|
||||
[ -n "${1}" ] && [ -z "${text}" ] && text=${1}
|
||||
;;
|
||||
chatupdate)
|
||||
[ -n "${1}" ] && [ -n "${text}" ] && [ -n "${timestamp}" ] && [ -z "${channel}" ] && channel=${1}
|
||||
[ -n "${1}" ] && [ -n "${text}" ] && [ -z "${timestamp}" ] && timestamp=${1}
|
||||
[ -n "${1}" ] && [ -z "${text}" ] && text=${1}
|
||||
;;
|
||||
filedelete|fileinfo)
|
||||
[ -n "${1}" ] && [ -z "${file}" ] && file=${1}
|
||||
;;
|
||||
fileupload)
|
||||
[ -n "${1}" ] && [ -n "${file}" ] && [ -z "${channels}" ] && channels=${1}
|
||||
[ -n "${1}" ] && [ -z "${file}" ] && file=${1}
|
||||
;;
|
||||
snoozeinfo)
|
||||
[ -n "${1}" ] && [ -z "${user}" ] && user=${1}
|
||||
;;
|
||||
snoozestart)
|
||||
[ -n "${1}" ] && [ -z "${minutes}" ] && minutes=${1}
|
||||
;;
|
||||
statusedit)
|
||||
[ -n "${1}" ] && [ -n "${text}" ] && [ -z "${emoji}" ] && emoji=${1}
|
||||
[ -n "${1}" ] && [ -z "${text}" ] && text=${1}
|
||||
;;
|
||||
reminderadd)
|
||||
[ -n "${1}" ] && [ -n "${text}" ] && [ -z "${_time}" ] && _time=${1}
|
||||
[ -n "${1}" ] && [ -z "${text}" ] && text=${1}
|
||||
;;
|
||||
remindercomplete|reminderdelete|reminderinfo)
|
||||
[ -n "${1}" ] && [ -z "${reminder}" ] && reminder=${1}
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# TRACING #########################################################################################
|
||||
${trace}
|
||||
|
||||
# ARGUMENT AND OPTION PROMPTING ###################################################################
|
||||
case "${cmd}${sub}" in
|
||||
chatdelete)
|
||||
[ -z "${channel}" ] && read -e -p 'Enter channel (e.g. #general): ' channel
|
||||
[ -z "${timestamp}" ] && read -e -p 'Enter timestamp (e.g. 1405894322.002768): ' timestamp
|
||||
;;
|
||||
chatsend)
|
||||
[ -z "${channel}" ] && [ -z "${text}" ] && prompt='true'
|
||||
[ -n "${prompt}" ] && [ -z "${actions}" ] && read -e -p 'Enter actions (e.g. {"type": “button", "style": "primary", "text": "my text", "url": "http://example.com"}, ...): ' actions
|
||||
[ -n "${prompt}" ] && [ -z "${author}" ] && read -e -p 'Enter author name (e.g. slackbot): ' author
|
||||
[ -n "${prompt}" ] && [ -z "${authoricon}" ] && read -e -p 'Enter author icon (e.g. a URL): ' authoricon
|
||||
[ -n "${prompt}" ] && [ -z "${authorlink}" ] && read -e -p 'Enter author link (e.g. a URL): ' authorlink
|
||||
[ -z "${channel}" ] && read -e -p 'Enter channel (e.g. #general): ' channel
|
||||
[ -n "${prompt}" ] && [ -z "${color}" ] && read -e -p 'Enter color (e.g. good): ' color
|
||||
[ -n "${prompt}" ] && [ -z "${fields}" ] && read -e -p 'Enter fields (e.g. {"title": "My Field Title", "value": "My field value", "short": true}, ...): ' fields
|
||||
[ -n "${prompt}" ] && [ -z "${footer}" ] && read -e -p 'Enter footer (e.g. Hello footer!): ' footer
|
||||
[ -n "${prompt}" ] && [ -z "${footericon}" ] && read -e -p 'Enter footer icon (e.g. a URL): ' footericon
|
||||
[ -n "${prompt}" ] && [ -z "${image}" ] && read -e -p 'Enter image (e.g. a URL): ' image
|
||||
[ -n "${prompt}" ] && [ -z "${pretext}" ] && read -e -p 'Enter pretext (e.g. Hello pretext!): ' pretext
|
||||
[ -z "${text}" ] && read -e -p 'Enter text (e.g. Hello text!): ' text
|
||||
[ -n "${prompt}" ] && [ -z "${thumbnail}" ] && read -e -p 'Enter thumbnail (e.g. a URL): ' thumbnail
|
||||
[ -n "${prompt}" ] && [ -z "${_time}" ] && read -e -p 'Enter time (e.g. 123456789): ' _time
|
||||
[ -n "${prompt}" ] && [ -z "${title}" ] && read -e -p 'Enter title (e.g. Hello title!): ' title
|
||||
[ -n "${prompt}" ] && [ -z "${titlelink}" ] && read -e -p 'Enter title link (e.g. a URL): ' titlelink
|
||||
;;
|
||||
chatupdate)
|
||||
[ -z "${text}" ] && read -e -p 'Enter text (e.g. Hello World!): ' text
|
||||
[ -z "${timestamp}" ] && read -e -p 'Enter timestamp (e.g. 1405894322.002768): ' timestamp
|
||||
[ -z "${channel}" ] && read -e -p 'Enter channel (e.g. #general): ' channel
|
||||
;;
|
||||
filedelete)
|
||||
[ -z "${file}" ] && read -e -p 'Enter file (e.g. F2147483862): ' file
|
||||
;;
|
||||
fileinfo)
|
||||
[ -z "${file}" ] && read -e -p 'Enter file (e.g. F2147483862): ' file
|
||||
;;
|
||||
fileupload)
|
||||
[ -z "${file}" ] && read -e -p 'Enter file (e.g. file.log): ' file
|
||||
[ -z "${channels}" ] && read -e -p 'Enter channels (e.g. #general,C1234567890): ' channels
|
||||
;;
|
||||
init)
|
||||
if [ -n "${SLACK_CLI_TOKEN}" ]; then
|
||||
_token="${SLACK_CLI_TOKEN}"
|
||||
elif [ -r "${etcdir}/.slack" ] && [ -f "${etcdir}/.slack" ]; then
|
||||
_token=$(sed -n '1p' < "${etcdir}/.slack")
|
||||
fi
|
||||
|
||||
if [ -z "${token}" ] || [ "${token}" == "${_token}" ]; then
|
||||
read -e -p 'Enter Slack API token: ' token
|
||||
fi
|
||||
;;
|
||||
reminderadd)
|
||||
[ -z "${text}" ] && read -e -p 'Enter text (e.g. lunch): ' text
|
||||
[ -z "${_time}" ] && read -e -p 'Enter time (e.g. 123456789): ' _time
|
||||
;;
|
||||
remindercomplete|reminderdelete|reminderinfo)
|
||||
[ -z "${reminder}" ] && read -e -p 'Enter reminder (e.g. RmCT7QGVBF): ' reminder
|
||||
;;
|
||||
snoozestart)
|
||||
[ -z "${minutes}" ] && read -e -p 'Enter minutes (e.g. 60): ' minutes
|
||||
;;
|
||||
statusedit)
|
||||
[ -z "${text}" ] && read -e -p 'Enter text (e.g. lunch): ' text
|
||||
[ -z "${emoji}" ] && read -e -p 'Enter emoji (e.g. :hamburger:): ' emoji
|
||||
;;
|
||||
esac
|
||||
|
||||
# COMMAND UTILITY FUNCTIONS #######################################################################
|
||||
function attachify() {
|
||||
[ -n "${actions}" ] && local _actions=", \"actions\": [${actions}]"
|
||||
[ -n "${author}" ] && local _author=", \"author_name\": \"${author}\""
|
||||
[ -n "${authoricon}" ] && local _authoricon=", \"author_icon\": \"${authoricon}\""
|
||||
[ -n "${authorlink}" ] && local _authorlink=", \"author_link\": \"${authorlink}\""
|
||||
[ -n "${color}" ] && local _color=", \"color\": \"${color}\""
|
||||
[ -n "${fields}" ] && local _fields=", \"fields\": [${fields}]"
|
||||
[ -n "${footer}" ] && local _footer=", \"footer\": \"${footer}\""
|
||||
[ -n "${footericon}" ] && local _footericon=", \"footer_icon\": \"${footericon}\""
|
||||
[ -n "${image}" ] && local _image=", \"image_url\": \"${image}\""
|
||||
[ -n "${pretext}" ] && local _pretext=", \"pretext\": \"${pretext}\""
|
||||
[ -n "${thumbnail}" ] && local _thumbnail=", \"thumb_url\": \"${thumbnail}\""
|
||||
[ -n "${_time}" ] && local __time=", \"ts\": \"${_time}\""
|
||||
[ -n "${title}" ] && local _title=", \"title\": \"${title}\""
|
||||
[ -n "${titlelink}" ] && local _titlelink=", \"title_link\": \"${titlelink}\""
|
||||
local _json="${_author}${_authoricon}${_authorlink}${_color}${_footer}${_footericon}${_image}${_pretext}${_thumbnail}${__time}${_title}${_titlelink}${_fields}${_actions}"
|
||||
[ -z "${_json}" ] && local _attachments="[{\"mrkdwn_in\": [\"pretext\"], \"fallback\": \"${text}\", \"pretext\": \"${text}\"}]"
|
||||
[ -n "${_json}" ] && local _attachments="[{\"mrkdwn_in\": [\"fields\", \"pretext\", \"text\"], \"fallback\": \"${text}\", \"text\": \"${text}\"${_json}}]"
|
||||
|
||||
echo "${_attachments}"
|
||||
}
|
||||
|
||||
function jqify() {
|
||||
case "$(echo ${1} | jq -r '.ok')" in
|
||||
true) echo ${1} | jq -r ${compact} ${monochrome} "${filter:=.}" ;;
|
||||
*) echo ${1} | jq -r ${compact} ${monochrome} "${filter:=.}" ; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function lchannel() {
|
||||
case "${channel}" in
|
||||
@*)
|
||||
local _user=$(\
|
||||
curl -s -X POST https://slack.com/api/users.list --data-urlencode "token=${token}" 2>&1 | \
|
||||
jq -r ".members | map(select(.name == \"${channel/@/}\" or .profile.display_name == \"${channel/@/}\")) | .[0].id")
|
||||
local _channel=$(\
|
||||
curl -s -X POST https://slack.com/api/im.list --data-urlencode "token=${token}" 2>&1 | \
|
||||
jq -r ".ims | map(select(.user == \"${_user}\")) | .[].id")
|
||||
|
||||
echo ${_channel}
|
||||
;;
|
||||
*) echo ${channel} ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function luser() {
|
||||
case "${user}" in
|
||||
@*)
|
||||
local _user=$(\
|
||||
curl -s -X POST https://slack.com/api/users.list --data-urlencode "token=${token}" 2>&1 | \
|
||||
jq -r ".members | map(select(.name == \"${user/@/}\" or .profile.display_name == \"${user/@/}\")) | .[0].id")
|
||||
|
||||
echo ${_user}
|
||||
;;
|
||||
*) echo ${user} ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# COMMAND FUNCTIONS ###############################################################################
|
||||
function chatdelete() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/chat.delete \
|
||||
--data-urlencode "as_user=true" \
|
||||
--data-urlencode "channel=$(lchannel)" \
|
||||
--data-urlencode "ts=${timestamp}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function chatsend() {
|
||||
[ -n "${stdin}" ] && [ -z "${text}" ] && text=\`\`\`${stdin//$'\n'/'\n'}\`\`\`
|
||||
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/chat.postMessage \
|
||||
--data-urlencode "as_user=true" \
|
||||
--data-urlencode "attachments=$(attachify)" \
|
||||
--data-urlencode "channel=$(lchannel)" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function chatupdate() {
|
||||
[ -n "${stdin}" ] && [ -z "${text}" ] && text=\`\`\`${stdin//$'\n'/'\n'}\`\`\`
|
||||
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/chat.update \
|
||||
--data-urlencode "as_user=true" \
|
||||
--data-urlencode "attachments=$(attachify)" \
|
||||
--data-urlencode "channel=$(lchannel)" \
|
||||
--data-urlencode "ts=${timestamp}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function help() {
|
||||
local a=(${0//\// })
|
||||
local bin=${a[${#a[@]}-1]}
|
||||
|
||||
echo 'Usage:'
|
||||
echo " ${bin} chat delete [<timestamp> [channel]]"
|
||||
echo ' [--channel|-ch <channel>] [--compact|-c] [--filter|-f <filter>] [--monochrome|-m]'
|
||||
echo ' [--timestamp|-ts <timestamp>] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} chat send [<text> [channel]]"
|
||||
echo ' [--author|-at <author>] [--author-icon|-ai <author-icon-url>]'
|
||||
echo ' [--author-link|-al <author-link>] [--channel|-ch <channel>] [--color|-cl <color>]'
|
||||
echo ' [--compact|-cp] [--fields|-flds <fields>] [--filter|-f <filter>] [--footer|-ft <footer>]'
|
||||
echo ' [--footer-icon|-fi <footer-icon-url>] [--image|-im <image-url>] [--monochrome|-m]'
|
||||
echo ' [--pretext|-pt <pretext>] [--text|-tx <text>] [--thumbnail|-th <thumbnail-url>]'
|
||||
echo ' [--time|-tm <time>] [--title|-ti <title>] [--title-link|-tl <title-link>]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} chat update [<text> [<timestamp> [channel]]]"
|
||||
echo ' [--author|-at <author>] [--author-icon|-ai <author-icon-url>]'
|
||||
echo ' [--author-link|-al <author-link>] [--channel|-ch <channel>] [--color|-cl <color>]'
|
||||
echo ' [--compact|-cp] [--fields|flds <fields>] [--filter|-f <filter>] [--footer|-ft <footer>]'
|
||||
echo ' [--footer-icon|-fi <footer-icon-url>] [--image|-im <image-url>] [--monochrome|-m]'
|
||||
echo ' [--pretext|-pt <pretext>] [--text|-tx <text>] [--thumbnail|-th <thumbnail-url>]'
|
||||
echo ' [--time|-tm <time>] [--timestamp|-ts <timestamp>] [--title|-ti <title>]'
|
||||
echo ' [--title-link|-tl <title-link>] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} init"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--token|-tk <token>]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} file delete [file]"
|
||||
echo ' [--compact|-c] [--file|-fl <file>] [--filter|-f <filter>] [--monochrome|-m]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} file info [file]"
|
||||
echo ' [--count|-cn <count>] [--compact|-c] [--file|-fl <file>] [--filter|-f <filter>]'
|
||||
echo ' [--monochrome|-m] [--page|-pg <page>] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} file list"
|
||||
echo ' [--channel|-ch <channel>] [--count|-cn <count>] [--compact|-c] [--filter|-f <filter>]'
|
||||
echo ' [--monochrome|-m] [--page|-pg <page>] [--timestamp-from|-tf <timetamp>]'
|
||||
echo ' [--timestamp-to|-tt <timestamp>] [--trace|-x] [--types|-ty <types>]'
|
||||
echo ' [--user|-ur <user>]'
|
||||
echo
|
||||
echo " ${bin} file upload [<file> [channels]]"
|
||||
echo ' [--channels|-chs <channels>] [--comment|-cm <comment>] [--compact|-c]'
|
||||
echo ' [--file|fl <file>] [--filename|-fn <filename>] [--filetype|-ft <filetype>]'
|
||||
echo ' [--filter|-f <filter>] [--monochrome|-m] [--title|-ti <title>] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} presence active"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} presence away"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} reminder add [<text> [time]]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--text|tx <text>]'
|
||||
echo ' [--time|tm <time>] [--trace|-x] [--user|-ur <user>]'
|
||||
echo
|
||||
echo " ${bin} reminder complete [reminder]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--reminder|rm <reminder>]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} reminder delete [reminder]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--reminder|rm <reminder>]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} reminder info [reminder]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--reminder|rm <reminder>]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} reminder list"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} snooze end"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} snooze info [user]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo ' [--user|-ur <user>]'
|
||||
echo
|
||||
echo " ${bin} snooze start [minutes]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--minutes|-mn <minutes>] [--monochrome|-m]'
|
||||
echo ' [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} status clear"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo
|
||||
echo " ${bin} status edit [<text> [<emoji>]]"
|
||||
echo ' [--compact|-c] [--filter|-f <filter>] [--monochrome|-m] [--trace|-x]'
|
||||
echo
|
||||
echo 'Configuration Commands:'
|
||||
echo ' init Initialize'
|
||||
echo
|
||||
echo 'Chat Commands:'
|
||||
echo ' chat delete Delete chat message'
|
||||
echo ' chat send Send chat message'
|
||||
echo ' chat update Update chat message'
|
||||
echo
|
||||
echo 'File Commands:'
|
||||
echo ' file delete Delete file'
|
||||
echo ' file info Info about file'
|
||||
echo ' file list List files'
|
||||
echo ' file upload Upload file'
|
||||
echo
|
||||
echo 'Presence Commands:'
|
||||
echo ' presence active Active presence'
|
||||
echo ' presence away Away presence'
|
||||
echo
|
||||
echo 'Reminder Commands:'
|
||||
echo ' reminder add Add reminder'
|
||||
echo ' reminder complete Complete reminder'
|
||||
echo ' reminder delete Delete reminder'
|
||||
echo ' reminder info Info about reminder'
|
||||
echo ' reminder list List reminders'
|
||||
echo
|
||||
echo 'Snooze Commands:'
|
||||
echo ' snooze end End snooze'
|
||||
echo ' snooze info Info about snooze'
|
||||
echo ' snooze start Start snooze'
|
||||
echo
|
||||
echo 'Status Commands:'
|
||||
echo ' status clear Clear status'
|
||||
echo ' status edit Edit status'
|
||||
echo
|
||||
echo 'More Information:'
|
||||
echo ' repo https://github.com/rockymadden/slack-cli'
|
||||
}
|
||||
|
||||
function filedelete() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/files.delete \
|
||||
--data-urlencode "file=${file}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function fileinfo() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/files.info \
|
||||
${count:+ --data-urlencode "count=${count}"} \
|
||||
--data-urlencode "file=${file}" \
|
||||
${page:+ --data-urlencode "page=${page}"} \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function filelist() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/files.list \
|
||||
${channel:+ --data-urlencode "channel=${channel}"} \
|
||||
${count:+ --data-urlencode "count=${count}"} \
|
||||
${page:+ --data-urlencode "page=${page}"} \
|
||||
${timestampfrom:+ --data-urlencode "ts_from=${timestampfrom}"} \
|
||||
${timestampto:+ --data-urlencode "ts_to=${timestampto}"} \
|
||||
${types:+ --data-urlencode "types=${types}"} \
|
||||
${user:+ --data-urlencode "user=$(luser)"} \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function fileupload() {
|
||||
[ -n "${stdin}" ] && [ -z "${file}" ] && file=${stdin}
|
||||
|
||||
if [ -f "${file}" ]; then
|
||||
local _file=${file}
|
||||
|
||||
case "${filename}" in
|
||||
'') local _filename=$(basename ${file}) ;;
|
||||
*) local _filename=${filename} ;;
|
||||
esac
|
||||
else
|
||||
local _content=${file}
|
||||
|
||||
case "${filename}" in
|
||||
'') local _filename='stdin' ;;
|
||||
*) local _filename=${filename} ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
case "${_file}" in
|
||||
'')
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/files.upload \
|
||||
${channels:+ --data-urlencode "channels=${channels}"} \
|
||||
${comment:+ --data-urlencode "initial_comment=${comment}"} \
|
||||
${_content:+ --data-urlencode "content=${_content}"} \
|
||||
${_filename:+ --data-urlencode "filename=${_filename}"} \
|
||||
${filetype:+ --data-urlencode "filetype=${filetype}"} \
|
||||
${title:+ --data-urlencode "title=${title}"} \
|
||||
--data-urlencode "token=${token}")
|
||||
;;
|
||||
*)
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/files.upload \
|
||||
${channels:+ --form-string "channels=${channels}"} \
|
||||
${comment:+ --form "initial_comment=${comment}"} \
|
||||
${_file:+ --form "file=@${_file}"} \
|
||||
${_filename:+ --form "filename=${_filename}"} \
|
||||
${filetype:+ --form "filetype=${filetype}"} \
|
||||
${title:+ --form "title=${title}"} \
|
||||
--form "token=${token}")
|
||||
;;
|
||||
esac
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function init() {
|
||||
echo "${token}" > "${etcdir}/.slack"
|
||||
|
||||
case "${?}" in
|
||||
0) echo '{"ok": true}' | jq -r ${compact} ${monochrome} "${filter:=.}" ;;
|
||||
*)
|
||||
echo '{"ok": false, "error": "not_writable"}' |
|
||||
jq -r ${compact} ${monochrome} "${filter:=.}" ; return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Setting presence=auto + setActive works more consistently than just setActive.
|
||||
function presenceactive() {
|
||||
local msg0=$(\
|
||||
curl -s -X POST https://slack.com/api/users.setPresence \
|
||||
--data-urlencode "presence=auto" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
local msg1=$(\
|
||||
curl -s -X POST https://slack.com/api/users.setActive \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg0}" && jqify "${msg1}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
function presenceaway() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/users.setPresence \
|
||||
--data-urlencode "presence=away" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function reminderadd() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/reminders.add \
|
||||
--data-urlencode "text=${text}" \
|
||||
--data-urlencode "time=${_time}" \
|
||||
${user:+ --data-urlencode "user=$(luser)"} \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function remindercomplete() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/reminders.complete \
|
||||
--data-urlencode "reminder=${reminder}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function reminderdelete() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/reminders.delete \
|
||||
--data-urlencode "reminder=${reminder}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function reminderinfo() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/reminders.info \
|
||||
--data-urlencode "reminder=${reminder}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function reminderlist() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/reminders.list \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function snoozeend() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function snoozeinfo() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/dnd.info \
|
||||
${user:+ --data-urlencode "user=$(luser)"} \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function snoozestart() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/dnd.setSnooze \
|
||||
--data-urlencode "num_minutes=${minutes}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function statusclear() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
--data-urlencode 'profile={"status_text":"", "status_emoji":""}' \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function statusedit() {
|
||||
local msg=$(\
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
--data-urlencode "profile={\"status_text\":\"${text}\", \"status_emoji\":\"${emoji}\"}" \
|
||||
--data-urlencode "token=${token}")
|
||||
|
||||
jqify "${msg}"
|
||||
}
|
||||
|
||||
function version() {
|
||||
echo '0.18.0'
|
||||
}
|
||||
|
||||
# COMMAND ROUTING #################################################################################
|
||||
case "${cmd}${sub}" in
|
||||
--help|-h) help ; exit 0 ;;
|
||||
--version|-v) version ; exit 0 ;;
|
||||
init) init ; exit $? ;;
|
||||
chatdelete|chatsend|chatupdate|\
|
||||
filedelete|fileinfo|filelist|fileupload|\
|
||||
presenceactive|presenceaway|\
|
||||
reminderadd|remindercomplete|reminderdelete|reminderinfo|reminderlist|\
|
||||
snoozeend|snoozeinfo|snoozestart|\
|
||||
statusclear|statusedit)
|
||||
if [ -z "${token}" ]; then
|
||||
echo '{"ok": false, "error": "not_inited"}' |
|
||||
jq -r ${compact} ${monochrome} "${filter:=.}" ; exit 1
|
||||
fi
|
||||
|
||||
${cmd}${sub} ; exit $?
|
||||
;;
|
||||
*) help ; exit 1 ;;
|
||||
esac
|
||||
2
bin/.local/bin/slr-to-webcam
Executable file
2
bin/.local/bin/slr-to-webcam
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
gphoto2 --stdout --capture-movie | gst-launch-1.0 fdsrc ! decodebin3 name=dec ! queue ! videoconvert ! v4l2sink device=/dev/video0
|
||||
5
bin/.local/bin/stow-plus
Executable file
5
bin/.local/bin/stow-plus
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
rm README.org
|
||||
rm screenshot.png
|
||||
stow *
|
||||
git checkout README.org screenshot.png
|
||||
8
bin/.local/bin/syncmail
Executable file
8
bin/.local/bin/syncmail
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $(pidof muchsync) ]; then
|
||||
echo "Checkmail is already running."
|
||||
else
|
||||
echo "Checking for new mail"
|
||||
muchsync mail0.local
|
||||
fi
|
||||
13
bin/.local/bin/sysupgrade
Executable file
13
bin/.local/bin/sysupgrade
Executable file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env sh
|
||||
# Upgrade all ubuntu hosts in one go
|
||||
echo "-------- Upgrading box.unbl.ink --------"
|
||||
ssh -t root@box.unbl.ink "apt upgrade -y && apt autoremove -y"
|
||||
echo "-------- Upgrading box.castine.town --------"
|
||||
ssh -t root@box.castine.town "apt upgrade -y && apt autoremove -y"
|
||||
echo "-------- Upgrading iapetus.local --------"
|
||||
ssh -J rhea.unbl.ink -t iapetus.local "sudo apt upgrade -y && sudo apt autoremove -y"
|
||||
echo "-------- Upgrading phoebe.local --------"
|
||||
ssh -J rhea.unbl.ink -t phoebe.local "sudo apt upgrade -y && sudo apt autoremove -y"
|
||||
echo "-------- Upgrading dione.local --------"
|
||||
ssh -J rhea.unbl.ink -t dione.local "sudo apt upgrade -y && sudo apt autoremove -y"
|
||||
# TODO Add titan & rhea freebsd upgrades
|
||||
28
bin/.local/bin/usb-move.sh
Executable file
28
bin/.local/bin/usb-move.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SRC_DIR="/run/media/powellc/SANSA CLIPZ"
|
||||
DEST_DIR="/home/powellc/var/rockbox/"
|
||||
FILENAME=".scrobbler.log"
|
||||
|
||||
WEBDAV_URL="https://box.unbl.ink/cloud/files/var/rockbox"
|
||||
WEBDAV_USER="colin@unbl.ink"
|
||||
|
||||
# Wait a moment to ensure mount is complete
|
||||
sleep 2
|
||||
|
||||
if [ -f "$SRC_DIR/$FILENAME" ]; then
|
||||
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
|
||||
NEW_NAME="scrobbles-$TIMESTAMP.tsv"
|
||||
|
||||
# Backup locally
|
||||
mv "$SRC_DIR/$FILENAME" "$DEST_DIR/$NEW_NAME"
|
||||
|
||||
# Send to WebDAV
|
||||
curl -s --netrc -T "$DEST_DIR/$NEW_NAME" "${WEBDAV_URL}/${NEW_NAME}"
|
||||
|
||||
# Notify user
|
||||
curl -s -d "Backed up $FILENAME locally and sent to Webdav" https://ntfy.unbl.ink/life >/dev/null
|
||||
else
|
||||
echo "File not found"
|
||||
fi
|
||||
50
bin/.local/bin/utm-backup.sh
Executable file
50
bin/.local/bin/utm-backup.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ---- CONFIG ----
|
||||
UTM_DIR="$HOME/Library/Containers/com.utmapp.UTM/Data/Documents"
|
||||
VM_NAME="Work.utm"
|
||||
|
||||
REMOTE_HOST="rhea.local"
|
||||
REMOTE_DIR="/tank/backups/utm"
|
||||
|
||||
KEEP=5
|
||||
DATE="$(date +%Y-%m-%d_%H-%M-%S)"
|
||||
|
||||
BASENAME="${VM_NAME%.utm}_$DATE"
|
||||
ARCHIVE="/tmp/$BASENAME.tar.gz"
|
||||
|
||||
# ---- SANITY CHECKS ----
|
||||
if [[ ! -d "$UTM_DIR/$VM_NAME" ]]; then
|
||||
echo "ERROR: VM not found: $UTM_DIR/$VM_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Optional: refuse if VM might be running
|
||||
if pgrep -f "$VM_NAME" >/dev/null; then
|
||||
echo "ERROR: VM appears to be running. Shut it down first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- CREATE ARCHIVE ----
|
||||
echo "Creating compressed archive..."
|
||||
tar -C "$UTM_DIR" -czf "$ARCHIVE" "$VM_NAME"
|
||||
|
||||
# ---- COPY ----
|
||||
echo "Uploading $ARCHIVE → $REMOTE_HOST:$REMOTE_DIR/"
|
||||
scp "$ARCHIVE" "$REMOTE_HOST:$REMOTE_DIR/"
|
||||
|
||||
# ---- REMOTE ROTATION ----
|
||||
echo "Pruning old backups on remote (keeping last $KEEP)..."
|
||||
|
||||
ssh "$REMOTE_HOST" "
|
||||
ls -dt $REMOTE_DIR/${VM_NAME%.utm}_*.tar.gz 2>/dev/null \
|
||||
| tail -n +$((KEEP + 1)) \
|
||||
| xargs -r rm -f
|
||||
"
|
||||
|
||||
# ---- CLEANUP ----
|
||||
rm -f "$ARCHIVE"
|
||||
|
||||
echo "Backup complete."
|
||||
|
||||
115
bin/.local/bin/webcam-build-timelapses
Executable file
115
bin/.local/bin/webcam-build-timelapses
Executable file
@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
# run_all_timelapses.sh
|
||||
#
|
||||
# Run timelapse creation for a fixed set of webcams,
|
||||
# collect public URLs, update HTML index, and send one combined ntfy notification.
|
||||
#
|
||||
# Requirements: make_timelapse.sh in same folder or PATH
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Settings ---
|
||||
BASE_DIR="/media/photos/misc/webcams"
|
||||
INDEX_FILE="$BASE_DIR/timelapses.html"
|
||||
WEBCAMS=(
|
||||
"frontyard"
|
||||
"basement_table"
|
||||
"mailbox"
|
||||
"basement_extension"
|
||||
"backyard_low"
|
||||
"backyard_north"
|
||||
"basement_cape"
|
||||
"basement_tv"
|
||||
"backyard"
|
||||
"bulkhead"
|
||||
"barn"
|
||||
"garage"
|
||||
"birds"
|
||||
"clouds"
|
||||
"garden"
|
||||
"diningroom"
|
||||
)
|
||||
TIMELAPSE_SCRIPT="/usr/local/bin/make_timelapse"
|
||||
|
||||
NTFY_URL="https://ntfy.unbl.ink/timelapse"
|
||||
WEB_PREFIX="https://files.lab.unbl.ink/webcams"
|
||||
# ----------------
|
||||
|
||||
# Yesterday's date
|
||||
if date -d "yesterday" +%Y >/dev/null 2>&1; then
|
||||
# GNU date (Linux)
|
||||
YESTERDAY=$(date -d "yesterday" +"%Y/%m/%d")
|
||||
else
|
||||
# BSD date (macOS)
|
||||
YESTERDAY=$(date -v-1d +"%Y/%m/%d")
|
||||
fi
|
||||
|
||||
LINKS=()
|
||||
|
||||
for cam in "${WEBCAMS[@]}"; do
|
||||
TARGET_DIR="$BASE_DIR/$cam/$YESTERDAY"
|
||||
|
||||
if [[ -d "$TARGET_DIR" ]]; then
|
||||
echo "📷 Processing $cam ($TARGET_DIR)"
|
||||
OUTPUT=$("$TIMELAPSE_SCRIPT" "$TARGET_DIR")
|
||||
echo "$OUTPUT"
|
||||
|
||||
LINK="$WEB_PREFIX/$cam/$YESTERDAY/timelapse.mp4"
|
||||
if [[ -f "$TARGET_DIR/timelapse.mp4" ]]; then
|
||||
LINKS+=("[$cam] $LINK")
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Skipping $cam (no folder for $YESTERDAY)"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Update index.html ---
|
||||
INDEX_HTML="$BASE_DIR/timelapses.html"
|
||||
|
||||
# Ensure file exists with basic structure
|
||||
if [[ ! -f "$INDEX_HTML" ]]; then
|
||||
cat > "$INDEX_HTML" <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Webcam Timelapses</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Webcam Timelapses</h1>
|
||||
<div id="days">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Build new day's section
|
||||
NEW_SECTION="<h2>$YESTERDAY</h2><ul>"
|
||||
for link in "${LINKS[@]}"; do
|
||||
cam=$(echo "$link" | cut -d']' -f1 | tr -d '[]')
|
||||
url=$(echo "$link" | awk '{print $2}')
|
||||
NEW_SECTION+="<li><a href=\"$url\">$cam</a></li>"
|
||||
done
|
||||
NEW_SECTION+="</ul>"
|
||||
|
||||
# Insert at the top of <div id="days">
|
||||
tmpfile=$(mktemp)
|
||||
awk -v section="$NEW_SECTION" '
|
||||
/<div id="days">/ { print; print section; next }
|
||||
{ print }
|
||||
' "$INDEX_HTML" > "$tmpfile"
|
||||
mv "$tmpfile" "$INDEX_HTML"
|
||||
|
||||
echo "📝 Updated index with $YESTERDAY timelapses (newest on top)"
|
||||
|
||||
# --- Send combined notification ---
|
||||
if [[ -n "$NTFY_URL" ]] && [[ ${#LINKS[@]} -gt 0 ]]; then
|
||||
MSG="Timelapses for $YESTERDAY:\n$(printf '%s\n' "${LINKS[@]}")"
|
||||
curl -s -H "Title: Timelapses complete" \
|
||||
-H "Priority: high" \
|
||||
-d "$MSG" \
|
||||
"$NTFY_URL" >/dev/null || true
|
||||
echo "📢 Combined notification sent to ntfy."
|
||||
fi
|
||||
|
||||
80
bin/.local/bin/webcam-capture
Executable file
80
bin/.local/bin/webcam-capture
Executable file
@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
if [ "$1" = "frontyard" ]; then
|
||||
host="loge.local"
|
||||
port=8081
|
||||
fi
|
||||
if [ "$1" = "basement_table" ]; then
|
||||
host="loge.local"
|
||||
port=8082
|
||||
fi
|
||||
if [ "$1" = "mailbox" ]; then
|
||||
host="loge.local"
|
||||
port=8083
|
||||
fi
|
||||
if [ "$1" = "basement_tv" ]; then
|
||||
port=8081
|
||||
host="polydeuces.local"
|
||||
fi
|
||||
if [ "$1" = "bulkhead" ]; then
|
||||
port=8081
|
||||
host="mimas.local"
|
||||
fi
|
||||
if [ "$1" = "basement_cape" ]; then
|
||||
port=8082
|
||||
host="mimas.local"
|
||||
fi
|
||||
if [ "$1" = "firewood" ]; then
|
||||
port=8083
|
||||
host="mimas.local"
|
||||
fi
|
||||
if [ "$1" = "backyard" ]; then
|
||||
port=8081
|
||||
host="mundilfari.local"
|
||||
fi
|
||||
if [ "$1" = "backyard_low" ]; then
|
||||
port=8082
|
||||
host="mundilfari.local"
|
||||
fi
|
||||
if [ "$1" = "basement_extension" ]; then
|
||||
port=8083
|
||||
host="mundilfari.local"
|
||||
fi
|
||||
if [ "$1" = "backyard_north" ]; then
|
||||
port=8084
|
||||
host="mundilfari.local"
|
||||
fi
|
||||
if [ "$1" = "barn" ]; then
|
||||
port=8081
|
||||
host="iapetus.local"
|
||||
fi
|
||||
if [ "$1" = "garage" ]; then
|
||||
port=8082
|
||||
host="iapetus.local"
|
||||
fi
|
||||
if [ "$1" = "porch" ]; then
|
||||
port=8083
|
||||
host="iapetus.local"
|
||||
fi
|
||||
if [ "$1" = "birds" ]; then
|
||||
port=8081
|
||||
host="siarnaq.local"
|
||||
fi
|
||||
if [ "$1" = "clouds" ]; then
|
||||
port=8082
|
||||
host="siarnaq.local"
|
||||
fi
|
||||
if [ "$1" = "garden" ]; then
|
||||
port=8083
|
||||
host="siarnaq.local"
|
||||
fi
|
||||
if [ "$1" = "diningroom" ]; then
|
||||
port=8081
|
||||
host="kari.local"
|
||||
fi
|
||||
|
||||
ROOT="/media/photos/misc/webcams"
|
||||
filepath="$1/$(date +%Y)/$(date +%m)/$(date +%d)/$(date +%Y%m%d%H%M%S).jpg"
|
||||
mkdir -p /$ROOT/$1/$(date +%Y)/$(date +%m)/$(date +%d)/
|
||||
curl $host:$port/snapshot -o /$ROOT/$filepath
|
||||
url="https://files.lab.unbl.ink/webcams/$filepath"
|
||||
#curl -H prio:low -H "click:$url" -d "Captured photo from $1 successfully" https://ntfy.unbl.ink/webcams
|
||||
19
bin/.local/bin/webcam-capture-all
Executable file
19
bin/.local/bin/webcam-capture-all
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
/home/powellc/.bin/webcam-capture frontyard
|
||||
/home/powellc/.bin/webcam-capture mailbox
|
||||
/home/powellc/.bin/webcam-capture basement_table
|
||||
/home/powellc/.bin/webcam-capture basement_tv
|
||||
/home/powellc/.bin/webcam-capture basement_extension
|
||||
/home/powellc/.bin/webcam-capture backyard_low
|
||||
/home/powellc/.bin/webcam-capture backyard_north
|
||||
/home/powellc/.bin/webcam-capture basement_cape
|
||||
/home/powellc/.bin/webcam-capture backyard
|
||||
/home/powellc/.bin/webcam-capture bulkhead
|
||||
/home/powellc/.bin/webcam-capture barn
|
||||
/home/powellc/.bin/webcam-capture garage
|
||||
/home/powellc/.bin/webcam-capture porch
|
||||
/home/powellc/.bin/webcam-capture birds
|
||||
/home/powellc/.bin/webcam-capture clouds
|
||||
/home/powellc/.bin/webcam-capture garden
|
||||
/home/powellc/.bin/webcam-capture diningroom
|
||||
49
bin/.local/bin/write_pdf_metadata_in_calibre.sh
Executable file
49
bin/.local/bin/write_pdf_metadata_in_calibre.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Scans the given directory and all subdirectories for the file
|
||||
# "metadata.opf". This file is created by Calibre. From this file data will be
|
||||
# written into the PDFs, which are exactly in the same directory.
|
||||
#
|
||||
#
|
||||
# REQUIREMENTS: calibre by KOVID GOYAL (http://calibre-ebook.com/)
|
||||
#
|
||||
|
||||
|
||||
|
||||
shopt -s nullglob
|
||||
shopt -s nocaseglob
|
||||
|
||||
|
||||
write_metadata() {
|
||||
find "$1" -depth -type f -name "metadata.opf" | { while read -r metadata_path;
|
||||
do
|
||||
echo "metadata found: $metadata_path"
|
||||
dir_name=$(dirname "$metadata_path")
|
||||
|
||||
pdfs=$(find "$dir_name" -maxdepth 1 -type f -name '*.pdf' | wc -l)
|
||||
|
||||
echo "pdfs found: $pdfs"
|
||||
|
||||
find "$dir_name" -maxdepth 1 -type f -name "*.pdf" | { while read -r pdf_path;
|
||||
do
|
||||
ebook-meta --from-opf="$metadata_path" "$pdf_path" 2>/dev/null
|
||||
done
|
||||
echo
|
||||
}
|
||||
done
|
||||
}
|
||||
}
|
||||
|
||||
[[ -z "$1" ]] && logError "Please specify a start directory." && exit 1
|
||||
|
||||
if [ -f "$1" ]
|
||||
then
|
||||
search_path=$(dirname "$1")
|
||||
else
|
||||
search_path="$1"
|
||||
fi
|
||||
|
||||
write_metadata "$search_path"
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user