#!/usr/bin/env bash function atfile.util.build_blob_uri() { did="$1" cid="$2" pds="$_server" echo "$_fmt_blob_url" | sed -e "s|\[pds\]|$pds|g" -e "s|\[server\]|$pds|g" -e "s|\[cid\]|$cid|g" -e "s|\[did\]|$did|g" } function atfile.util.build_out_filename() { key="$1" name="$2" # shellcheck disable=SC2154 echo "$_fmt_out_file" | sed -e "s|\[name\]|$name|g" -e "s|\[key\]|$key|g" } function atfile.util.build_query_array() { key="$1" values="$2" unset query if [[ -n "$values" ]]; then while IFS=$";" read -ra values_array; do for value in "${values_array[@]}"; do query+="$key=$value&" done done <<< "$values" fi echo "$query" } function atfile.util.check_prog() { command="$1" download_hint="$2" skip_hint="$3" atfile.say.debug "Checking program '$1' exists..." if ! [ -x "$(command -v "$command")" ]; then message="'$command' not installed" if [[ -n "$download_hint" ]]; then if [[ "$download_hint" == "http"* ]]; then message="$message (download: $download_hint)" else message="$message (install: \`$download_hint\`)" fi fi if [[ -n "$skip_hint" ]]; then message="$message\n↳ This is optional; set ${skip_hint}=1 to ignore" fi atfile.die "$message" fi } function atfile.util.check_prog_gpg() { atfile.util.check_prog "gpg" "https://gnupg.org/download" } function atfile.util.check_prog_optional_metadata() { # shellcheck disable=SC2154 [[ $_disable_ni_exiftool == 0 ]] && atfile.util.check_prog "exiftool" "https://exiftool.org" "${_envvar_prefix}_DISABLE_NI_EXIFTOOL" # shellcheck disable=SC2154 [[ $_disable_ni_mediainfo == 0 ]] && atfile.util.check_prog "mediainfo" "https://mediaarea.net/en/MediaInfo" "${_envvar_prefix}_DISABLE_NI_MEDIAINFO" } function atfile.util.create_dir() { dir="$1" atfile.say.debug "Creating directory '$dir'..." if ! [[ -d $dir ]]; then mkdir -p "$dir" # shellcheck disable=SC2181 [[ $? != 0 ]] && atfile.die "Unable to create directory '$dir'" fi } function atfile.util.fmt_int() { printf "%'d\n" "$1" } function atfile.util.get_cache_path() { # shellcheck disable=SC2154 mkdir -p "$_path_cache" # shellcheck disable=SC2154 echo "$_path_cache/$1" } function atfile.util.get_cdn_uri() { did="$1" blob_cid="$2" type="$3" cdn_uri="" case $type in "image/jpeg"|"image/png") cdn_uri="https://cdn.bsky.app/img/feed_thumbnail/plain/$did/$blob_cid@jpeg" ;; esac echo "$cdn_uri" } function atfile.util.get_ci() { [[ -d "/tangled/workspace" ]] && echo "tangled" } # TODO: Support BusyBox's shit `date` command # `date -u +"$format" -s "1996-08-11 01:23:34"` function atfile.util.get_date() { date="$1" format="$2" unset in_format [[ -z $format ]] && format="%Y-%m-%dT%H:%M:%SZ" if [[ $date =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{3}){0,1})Z$ ]]; then if [[ $_os == "bsd" ]]; then date="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" in_format="%Y-%m-%d %H:%M:%S" elif [[ $_os == "linux-musl" || $_os == "solaris" ]]; then date="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" fi fi [[ -z $in_format ]] && in_format="$format" if [[ -z "$date" ]]; then if [[ $_os == "linux-musl" || $_os == "solaris" ]]; then echo "" else date -u +"$format" fi else if [[ $_os == "linux-musl" || $_os == "solaris" ]]; then date -u -d "$date" elif [[ $_os == "bsd" || $_os == "macos" ]]; then date -u -j -f "$in_format" "$date" +"$format" else date --date "$date" -u +"$format" fi fi } function atfile.util.get_date_json() { date="$1" parsed="$2" if [[ -z "$parsed" ]]; then if [[ -n "$date" ]]; then parsed_date="$(atfile.util.get_date "$date" 2> /dev/null)" # shellcheck disable=SC2181 [[ $? == 0 ]] && parsed="$parsed_date" fi fi if [[ -n "$parsed" ]]; then echo "\"$parsed\"" else echo "null" fi } function atfile.util.get_didplc_doc() { actor="$1" function atfile.util.get_didplc_doc.request_doc() { endpoint="$1" actor="$2" curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$endpoint/$actor" } # shellcheck disable=SC2154 didplc_endpoint="$_endpoint_plc_directory" didplc_doc="$(atfile.util.get_didplc_doc.request_doc "$didplc_endpoint" "$actor")" if [[ "$didplc_doc" != "{"* ]]; then # shellcheck disable=SC2154 didplc_endpoint="https://plc.directory" didplc_doc="$(atfile.util.get_didplc_doc.request_doc "$didplc_endpoint" "$actor")" fi echo "$didplc_doc" | jq ". += {\"directory\": \"$didplc_endpoint\"}" } function atfile.util.get_didweb_doc_url() { actor="$1" echo "https://${actor//did:web:/}/.well-known/did.json" } function atfile.util.get_envvar() { envvar="$1" default="$2" envvar_from_envfile="$(atfile.util.get_envvar_from_envfile "$envvar")" envvar_value="" if [[ -n "${!envvar}" ]]; then envvar_value="${!envvar}" elif [[ -n "$envvar_from_envfile" ]]; then envvar_value="$envvar_from_envfile" fi if [[ -z "$envvar_value" ]]; then envvar_value="$default" fi echo "$envvar_value" } function atfile.util.get_envvar_from_envfile() { variable="$1" [[ -f $_path_envvar ]] && atfile.util.get_var_from_file "$_path_envvar" "$variable" } function atfile.util.get_exiftool_field() { file="$1" tag="$2" default="$3" output="" exiftool_output="$(eval "exiftool -c \"%+.6f\" -s -T -$tag \"$file\"")" if [[ -n "$exiftool_output" ]]; then if [[ "$exiftool_output" == "-" ]]; then output="$default" else output="$exiftool_output" fi else output="$default" fi echo "$(echo "$output" | sed "s|\"|\\\\\"|g")" } function atfile.util.get_file_name_pretty() { file_record="$1" emoji="$(atfile.util.get_file_type_emoji "$(echo "$file_record" | jq -r '.file.mimeType')")" file_name_no_ext="$(echo "$file_record" | jq -r ".file.name" | cut -d "." -f 1)" output="$file_name_no_ext" meta_type="$(echo "$file_record" | jq -r ".meta.\"\$type\"")" if [[ -n "$meta_type" ]]; then case $meta_type in "$_nsid_meta#audio") album="$(echo "$file_record" | jq -r ".meta.tags.album")" album_artist="$(echo "$file_record" | jq -r ".meta.tags.album_artist")" date="$(echo "$file_record" | jq -r ".meta.tags.date")" disc="$(echo "$file_record" | jq -r ".meta.tags.disc.position")" title="$(echo "$file_record" | jq -r ".meta.tags.title")" track="$(echo "$file_record" | jq -r ".meta.tags.track.position")" [[ $(atfile.util.is_null_or_empty "$album") == 1 ]] && album="(Unknown Album)" [[ $(atfile.util.is_null_or_empty "$album_artist") == 1 ]] && album_artist="(Unknown Artist)" [[ $(atfile.util.is_null_or_empty "$disc") == 1 ]] && disc=0 [[ $(atfile.util.is_null_or_empty "$title") == 1 ]] && title="$file_name_no_ext" [[ $(atfile.util.is_null_or_empty "$track") == 1 ]] && track=0 output="$title\n $album_artist — $album" [[ $(atfile.util.is_null_or_empty "$date") == 0 ]] && output+=" ($(atfile.util.get_date "$date" "%Y"))" [[ $disc != 0 || $track != 0 ]] && output+=" [$disc.$track]" ;; "$_nsid_meta#photo") date="$(echo "$file_record" | jq -r ".meta.date.create")" lat="$(echo "$file_record" | jq -r ".meta.gps.lat")" long="$(echo "$file_record" | jq -r ".meta.gps.long")" title="$(echo "$file_record" | jq -r ".meta.title")" [[ -z "$title" ]] && title="$file_name_no_ext" output="$title" if [[ $(atfile.util.is_null_or_empty "$lat") == 0 && $(atfile.util.is_null_or_empty "$long") == 0 ]]; then output+="\n $long $lat" if [[ $(atfile.util.is_null_or_empty "$date") == 0 ]]; then output+=" — $(atfile.util.get_date "$date")" fi fi ;; "$_nsid_meta#video") title="$(echo "$file_record" | jq -r ".meta.tags.title")" [[ $(atfile.util.is_null_or_empty "$title") == 1 ]] && title="$file_name_no_ext" output="$title" ;; esac fi # BUG: Haiku Terminal has issues with emojis if [[ $_os != "haiku" ]]; then output="$emoji $output" fi output_last_line="$(echo -e "$output" | tail -n1)" output_last_line_length="${#output_last_line}" echo -e "$output" echo -e "$(atfile.util.repeat_char "-" "$output_last_line_length")" } function atfile.util.get_file_size_pretty() { size="$1" suffix="" if (( size >= 1048576 )); then size=$(( size / 1048576 )) suffix="MiB" elif (( size >= 1024 )); then size=$(( size / 1024 )) suffix="KiB" else suffix="B" fi echo "$size $suffix" } # NOTE: There is currently no API for getting the filesize limit on the server function atfile.util.get_file_size_surplus_for_pds() { size="$1" pds="$2" unset max_filesize case $pds in *".host.bsky.network") max_filesize=1073741824 ;; esac if [[ -z $max_filesize ]] || [[ $max_filesize == 0 ]] || (( size < max_filesize )); then echo 0 else echo $(( size - max_filesize )) fi } function atfile.util.get_file_type_emoji() { mime_type="$1" short_type="$(echo "$mime_type" | cut -d "/" -f 1)" desc_type="$(echo "$mime_type" | cut -d "/" -f 2)" case $short_type in "application") case "$desc_type" in # Apps (Desktop) "vnd.debian.binary-package"| \ "vnd.microsoft.portable-executable"| \ "x-executable"| \ "x-rpm") echo "šŸ’»" ;; # Apps (Mobile) "vnd.android.package-archive"| \ "x-ios-app") echo "šŸ“±" ;; # Archives "prs.atfile.car"| \ "gzip"|"x-7z-compressed"|"x-apple-diskimage"|"x-bzip2"|"x-stuffit"|"x-xz"|"zip") echo "šŸ“¦" ;; # Disk Images "x-iso9660-image") echo "šŸ’æ" ;; # Encrypted "prs.atfile.gpg-crypt") echo "šŸ”‘" ;; # Rich Text "pdf"| \ "vnd.oasis.opendocument.text") echo "šŸ“„" ;; *) echo "āš™ļø " ;; esac ;; "audio") echo "šŸŽµ" ;; "font") echo "āœļø" ;; "image") echo "šŸ–¼ļø " ;; "inode") echo "šŸ”Œ" ;; "text") case "$mime_type" in "text/x-shellscript") echo "āš™ļø " ;; *) echo "šŸ“„" ;; esac ;; "video") echo "šŸ“¼" ;; *) echo "ā“" ;; esac } function atfile.util.get_int_suffix() { int="$1" singular="$2" plural="$3" [[ $int == 1 ]] && echo -e "$singular" || echo -e "$plural" } function atfile.util.get_finger_record() { fingerprint_override="$1" unset enable_fingerprint_original if [[ $fingerprint_override ]]; then enable_fingerprint_original="$_enable_fingerprint" _enable_fingerprint="$fingerprint_override" fi echo -e "$(blue.zio.atfile.finger__machine)" if [[ -n $enable_fingerprint_original ]]; then _enable_fingerprint="$enable_fingerprint_original" fi } function atfile.util.get_line() { input="$1" index=$(( $2 + 1 )) echo -e "$input" | sed -n "$(( index ))"p } function atfile.util.get_mediainfo_field() { file="$1" category="$2" field="$3" default="$4" output="" mediainfo_output="$(mediainfo --Inform="$category;%$field%\n" "$file")" if [[ -n "$mediainfo_output" ]]; then if [[ "$mediainfo_output" == "None" ]]; then output="$default" else output="$mediainfo_output" fi else output="$default" fi echo "$(echo "$output" | sed "s|\"|\\\\\"|g")" } function atfile.util.get_mediainfo_audio_json() { file="$1" bitRates=$(atfile.util.get_mediainfo_field "$file" "Audio" "BitRate" 0) bitRate_modes=$(atfile.util.get_mediainfo_field "$file" "Audio" "BitRate_Mode" "") channelss=$(atfile.util.get_mediainfo_field "$file" "Audio" "Channels" 0) compressions="$(atfile.util.get_mediainfo_field "$file" "Audio" "Compression_Mode" "")" durations=$(atfile.util.get_mediainfo_field "$file" "Audio" "Duration" 0) formats="$(atfile.util.get_mediainfo_field "$file" "Audio" "Format" "")" format_ids="$(atfile.util.get_mediainfo_field "$file" "Audio" "CodecID" "")" format_profiles="$(atfile.util.get_mediainfo_field "$file" "Audio" "Format_Profile" "")" samplings=$(atfile.util.get_mediainfo_field "$file" "Audio" "SamplingRate" 0) titles="$(atfile.util.get_mediainfo_field "$file" "Audio" "Title" "")" lines="$(echo "$bitRates" | wc -l)" output="" for (( i = 0 ; i < lines ; i++ )); do lossy=true [[ $(atfile.util.get_line "$compressions" $i) == "Lossless" ]] && lossy=false output+="{ \"bitRate\": $(atfile.util.get_line "$bitRates" $i), \"channels\": $(atfile.util.get_line "$channelss" $i), \"duration\": $(atfile.util.get_line "$durations" $i), \"format\": { \"id\": \"$(atfile.util.get_line "$format_ids" $i)\", \"name\": \"$(atfile.util.get_line "$formats" $i)\", \"profile\": \"$(atfile.util.get_line "$format_profiles" $i)\" }, \"mode\": \"$(atfile.util.get_line "$bitRate_modes" $i)\", \"lossy\": $lossy, \"sampling\": $(atfile.util.get_line "$samplings" $i), \"title\": \"$(atfile.util.get_line "$titles" $i)\" }," done echo "${output::-1}" } function atfile.util.get_mediainfo_video_json() { file="$1" bitRates=$(atfile.util.get_mediainfo_field "$file" "Video" "BitRate" 0) dim_height=$(atfile.util.get_mediainfo_field "$file" "Video" "Height" 0) dim_width=$(atfile.util.get_mediainfo_field "$file" "Video" "Width" 0) durations=$(atfile.util.get_mediainfo_field "$file" "Video" "Duration" 0) formats="$(atfile.util.get_mediainfo_field "$file" "Video" "Format" "")" format_ids="$(atfile.util.get_mediainfo_field "$file" "Video" "CodecID" "")" format_profiles="$(atfile.util.get_mediainfo_field "$file" "Video" "Format_Profile" "")" frameRates="$(atfile.util.get_mediainfo_field "$file" "Video" "FrameRate" "")" frameRate_modes="$(atfile.util.get_mediainfo_field "$file" "Video" "FrameRate_Mode" "")" titles="$(atfile.util.get_mediainfo_field "$file" "Video" "Title" "")" lines="$(echo "$bitRates" | wc -l)" output="" for ((i = 0 ; i < lines ; i++ )); do output+="{ \"bitRate\": $(atfile.util.get_line "$bitRates" $i), \"dimensions\": { \"height\": $dim_height, \"width\": $dim_width }, \"duration\": $(atfile.util.get_line "$durations" $i), \"format\": { \"id\": \"$(atfile.util.get_line "$format_ids" $i)\", \"name\": \"$(atfile.util.get_line "$formats" $i)\", \"profile\": \"$(atfile.util.get_line "$format_profiles" $i)\" }, \"frameRate\": $(atfile.util.get_line "$frameRates" $i), \"mode\": \"$(atfile.util.get_line "$frameRate_modes" $i)\", \"title\": \"$(atfile.util.get_line "$titles" $i)\" }," done echo "${output::-1}" } function atfile.util.get_meta_record() { file="$1" type="$2" case "$type" in "audio/"*) blue.zio.atfile.meta__audio "$1" ;; "image/"*) blue.zio.atfile.meta__photo "$1" ;; "video/"*) blue.zio.atfile.meta__video "$1" ;; *) blue.zio.atfile.meta__unknown "" "$type" ;; esac } function atfile.util.get_md5() { file="$1" unset checksum type="none" if [ -x "$(command -v md5sum)" ]; then hash="$(md5sum "$file" | cut -f 1 -d " ")" if [[ ${#hash} == 32 ]]; then checksum="$hash" type="md5" fi fi echo "$checksum|$type" } function atfile.util.get_os() { os="${OSTYPE,,}" [[ -n $_force_os ]] && os="force-${_force_os,,}" # shellcheck disable=SC2221 # shellcheck disable=SC2222 case $os in # BSD "freebsd"*|"netbsd"*|"openbsd"*|*"bsd"|"force-bsd") echo "bsd" ;; # Haiku "haiku"|"force-haiku") echo "haiku" ;; # Linux "linux-gnu"|"force-linux") echo "linux" ;; "cygwin"|"msys"|"force-linux-mingw") echo "linux-mingw" ;; "linux-musl"|"force-linux-musl") echo "linux-musl" ;; "linux-android"|"force-linux-termux") echo "linux-termux" ;; # macOS "darwin"*|"force-macos") echo "macos" ;; # SerenityOS "serenity"*|"force-serenity") echo "serenity" ;; # Solaris "solaris"*|"force-solaris") echo "solaris" ;; # Unknown *) echo "unknown-${os//force-/}" ;; esac } function atfile.util.get_pds_pretty() { pds="$1" pds_host="$(atfile.util.get_uri_segment "$pds" host)" unset pds_name unset pds_emoji case "$pds_host" in *".host.bsky.network") bsky_host="$(echo "$pds_host" | cut -d "." -f 1)" bsky_region="$(echo "$pds_host" | cut -d "." -f 2)" pds_name="${bsky_host^} ($(atfile.util.get_region_pretty "$bsky_region"))" pds_emoji="šŸ„" ;; "at.app.wafrn.net") pds_name="Wafrn"; pds_emoji="🌸" ;; "atproto.brid.gy") pds_name="Bridgy Fed"; pds_emoji="šŸ”€" ;; "blacksky.app") pds_name="Blacksky"; pds_emoji="⬛" ;; "pds.sprk.so") pds_name="Spark"; pds_emoji="✨" ;; "tngl.sh") pds_name="Tangled"; pds_emoji="🪢" ;; *) pds_oauth_url="$pds/oauth/authorize" pds_oauth_page="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$pds_oauth_url" | tr -d '\n')" pds_customization_data="$(echo "$pds_oauth_page" | sed -n 's/.*window\["__customizationData"\]=JSON.parse("\(.*\)");.*/\1/p' | sed 's/\\"/"/g; s/\\\\/\\/g' | sed 's/");window\[".*$//')" if [[ $pds_customization_data == "{"* ]]; then pds_name="$(echo "$pds_customization_data" | jq -r '.name')" pds_emoji="🟦" else pds_name="$pds_host" fi ;; esac # BUG: Haiku Terminal has issues with emojis if [[ -n "$pds_emoji" ]] && [[ $_os != "haiku" ]]; then echo "$pds_emoji $pds_name" else echo "$pds_name" fi } function atfile.util.get_random() { amount="$1" [[ -z "$amount" ]] && amount="6" echo "$(tr -dc A-Za-z0-9 = _max_list )); then first_line="List is limited to $_max_list results. To print more results," first_line_length=$(( ${#first_line} + 3 )) # shellcheck disable=SC2154 echo -e "$(atfile.util.repeat_char "-" $first_line_length)\nā„¹ļø $first_line\n run \`$_prog $_command $cursor\`" fi } function atfile.util.repeat_char() { char="$1" amount="$2" if [ -x "$(command -v seq)" ]; then printf "%0.s$char" $(seq 1 "$amount") else echo "$char" fi } function atfile.util.resolve_identity() { actor="$1" if [[ "$actor" != "did:"* ]]; then # shellcheck disable=SC2154 resolved_handle="$(atfile.xrpc.bsky.get "com.atproto.identity.resolveHandle" "handle=$actor")" error="$(atfile.util.get_xrpc_error $? "$resolved_handle")" if [[ -z "$error" ]]; then actor="$(echo "$resolved_handle" | jq -r ".did")" fi fi if [[ "$actor" == "did:"* ]]; then unset did_doc case "$actor" in "did:plc:"*) did_doc="$(atfile.util.get_didplc_doc "$actor")" ;; "did:web:"*) did_doc="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$(atfile.util.get_didweb_doc_url "$actor")")" ;; *) echo "Unknown DID type 'did:$(echo "$actor" | cut -d ":" -f 2)'"; exit 255;; esac if [[ -n "$did_doc" ]]; then did="$(echo "$did_doc" | jq -r ".id")" if [[ $(atfile.util.is_null_or_empty "$did") == 1 ]]; then echo "$error" exit 255 fi unset aliases unset handle didplc_dir="$(echo "$did_doc" | jq -r ".directory")" pds="$(echo "$did_doc" | jq -r '.service[] | select(.id == "#atproto_pds") | .serviceEndpoint')" while IFS=$'\n' read -r a; do aliases+="$a;" if [[ -z $handle && "$a" == "at://"* && "$a" != "at://did:"* ]]; then handle="$a" fi done <<< "$(echo "$did_doc" | jq -r '.alsoKnownAs[]')" [[ $didplc_dir == "null" ]] && unset didplc_dir [[ -z "$handle" ]] && handle="invalid.handle" echo "$did|$pds|$handle|$didplc_dir|$aliases" fi else echo "$error" exit 255 fi } function atfile.util.source_hook() { file="$1" if [[ -n "$file" ]]; then atfile.say.debug "Sourcing: $file" if [[ -f "$file" ]]; then # shellcheck disable=SC1090 . "$file" else atfile.die "Unable to source '$file'" fi fi } function atfile.util.write_cache() { file="$1" file_path="$_path_cache/$1" content="$2" atfile.util.get_cache "$file" echo -e "$content" > "$file_path" # shellcheck disable=SC2320 # shellcheck disable=SC2181 [[ $? != 0 ]] && atfile.die "Unable to write to cache file ($file)" }