[bin] Put bin files in .local/bin

This commit is contained in:
2026-02-10 12:12:11 -05:00
parent c50d88ce26
commit d1eb0f5b3d
45 changed files with 0 additions and 0 deletions

View File

@ -1,352 +0,0 @@
#!/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

View File

@ -1,19 +0,0 @@
#!/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 ~/.

Binary file not shown.

View File

@ -1,65 +0,0 @@
#!/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

View File

@ -1,15 +0,0 @@
#!/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

View File

@ -1,111 +0,0 @@
#!/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

View File

@ -1,205 +0,0 @@
#!/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)

View File

@ -1,105 +0,0 @@
#!/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"

View File

@ -1,2 +0,0 @@
#!/bin/sh
~/.emacs.d/bin/doom $1

Binary file not shown.

View File

@ -1,22 +0,0 @@
#!/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

View File

@ -1,37 +0,0 @@
#!/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)

View File

@ -1,44 +0,0 @@
#!/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)

View File

@ -1,34 +0,0 @@
#!/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.")

View File

@ -1,24 +0,0 @@
#!/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)

View File

@ -1,66 +0,0 @@
#!/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

View File

@ -1,14 +0,0 @@
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']"

View File

@ -1,7 +0,0 @@
#!/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

View File

@ -1,253 +0,0 @@
#!/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()

View File

@ -1,14 +0,0 @@
#!/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"

View File

@ -1,491 +0,0 @@
#! /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:])

Binary file not shown.

View File

@ -1,3 +0,0 @@
#!/bin/sh
export KITTY_DISABLE_WAYLAND=1
exec /usr/bin/kitty

View File

@ -1,42 +0,0 @@
#!/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}*")

View File

@ -1,73 +0,0 @@
# /// 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()

View File

@ -1,18 +0,0 @@
#!/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

View File

@ -1,19 +0,0 @@
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])

View File

@ -1,17 +0,0 @@
#!/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

View File

@ -1,33 +0,0 @@
#!/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

View File

@ -1,29 +0,0 @@
#!/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 "$@"

View File

@ -1,56 +0,0 @@
#!/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)

View File

@ -1,70 +0,0 @@
#!/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

View File

@ -1,3 +0,0 @@
#!/bin/sh
snapclient -h snapcast.service &

View File

@ -1,8 +0,0 @@
#!/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

View File

@ -1,682 +0,0 @@
#!/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

View File

@ -1,2 +0,0 @@
#!/bin/bash
gphoto2 --stdout --capture-movie | gst-launch-1.0 fdsrc ! decodebin3 name=dec ! queue ! videoconvert ! v4l2sink device=/dev/video0

View File

@ -1,5 +0,0 @@
#!/bin/sh
rm README.org
rm screenshot.png
stow *
git checkout README.org screenshot.png

View File

@ -1,8 +0,0 @@
#!/bin/bash
if [ $(pidof muchsync) ]; then
echo "Checkmail is already running."
else
echo "Checking for new mail"
muchsync mail0.local
fi

View File

@ -1,13 +0,0 @@
#!/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

View File

@ -1,28 +0,0 @@
#!/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

View File

@ -1,50 +0,0 @@
#!/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."

View File

@ -1,115 +0,0 @@
#!/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

View File

@ -1,80 +0,0 @@
#!/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

View File

@ -1,19 +0,0 @@
#!/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

View File

@ -1,49 +0,0 @@
#!/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