1#!/usr/bin/env bash
2
3function atfile.util.build_blob_uri() {
4 did="$1"
5 cid="$2"
6 pds="$_server"
7
8 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"
9}
10
11function atfile.util.build_out_filename() {
12 key="$1"
13 name="$2"
14
15 # shellcheck disable=SC2154
16 echo "$_fmt_out_file" | sed -e "s|\[name\]|$name|g" -e "s|\[key\]|$key|g"
17}
18
19function atfile.util.build_query_array() {
20 key="$1"
21 values="$2"
22
23 unset query
24
25 if [[ -n "$values" ]]; then
26 while IFS=$";" read -ra values_array; do
27 for value in "${values_array[@]}"; do
28 query+="$key=$value&"
29 done
30 done <<< "$values"
31 fi
32
33 echo "$query"
34}
35
36function atfile.util.check_prog() {
37 command="$1"
38 download_hint="$2"
39 skip_hint="$3"
40
41 atfile.say.debug "Checking program '$1' exists..."
42
43 if ! [ -x "$(command -v "$command")" ]; then
44 message="'$command' not installed"
45
46 if [[ -n "$download_hint" ]]; then
47 if [[ "$download_hint" == "http"* ]]; then
48 message="$message (download: $download_hint)"
49 else
50 message="$message (install: \`$download_hint\`)"
51 fi
52 fi
53
54 if [[ -n "$skip_hint" ]]; then
55 message="$message\n↳ This is optional; set ${skip_hint}=1 to ignore"
56 fi
57
58 atfile.die "$message"
59 fi
60}
61
62function atfile.util.check_prog_gpg() {
63 atfile.util.check_prog "gpg" "https://gnupg.org/download"
64}
65
66function atfile.util.check_prog_optional_metadata() {
67 # shellcheck disable=SC2154
68 [[ $_disable_ni_exiftool == 0 ]] && atfile.util.check_prog "exiftool" "https://exiftool.org" "${_envvar_prefix}_DISABLE_NI_EXIFTOOL"
69 # shellcheck disable=SC2154
70 [[ $_disable_ni_mediainfo == 0 ]] && atfile.util.check_prog "mediainfo" "https://mediaarea.net/en/MediaInfo" "${_envvar_prefix}_DISABLE_NI_MEDIAINFO"
71}
72
73function atfile.util.create_dir() {
74 dir="$1"
75
76 atfile.say.debug "Creating directory '$dir'..."
77
78 if ! [[ -d $dir ]]; then
79 mkdir -p "$dir"
80 # shellcheck disable=SC2181
81 [[ $? != 0 ]] && atfile.die "Unable to create directory '$dir'"
82 fi
83}
84
85function atfile.util.fmt_int() {
86 printf "%'d\n" "$1"
87}
88
89function atfile.util.get_cache_path() {
90 # shellcheck disable=SC2154
91 mkdir -p "$_path_cache"
92 # shellcheck disable=SC2154
93 echo "$_path_cache/$1"
94}
95
96function atfile.util.get_cdn_uri() {
97 did="$1"
98 blob_cid="$2"
99 type="$3"
100
101 cdn_uri=""
102
103 case $type in
104 "image/jpeg"|"image/png") cdn_uri="https://cdn.bsky.app/img/feed_thumbnail/plain/$did/$blob_cid@jpeg" ;;
105 esac
106
107 echo "$cdn_uri"
108}
109
110function atfile.util.get_ci() {
111 [[ -d "/tangled/workspace" ]] && echo "tangled"
112}
113
114# TODO: Support BusyBox's shit `date` command
115# `date -u +"$format" -s "1996-08-11 01:23:34"`
116function atfile.util.get_date() {
117 date="$1"
118 format="$2"
119 unset in_format
120
121 [[ -z $format ]] && format="%Y-%m-%dT%H:%M:%SZ"
122
123 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
124 if [[ $_os == "bsd" ]]; then
125 date="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
126 in_format="%Y-%m-%d %H:%M:%S"
127 elif [[ $_os == "linux-musl" || $_os == "solaris" ]]; then
128 date="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
129 fi
130 fi
131
132 [[ -z $in_format ]] && in_format="$format"
133
134 if [[ -z "$date" ]]; then
135 if [[ $_os == "linux-musl" || $_os == "solaris" ]]; then
136 echo ""
137 else
138 date -u +"$format"
139 fi
140 else
141 if [[ $_os == "linux-musl" || $_os == "solaris" ]]; then
142 date -u -d "$date"
143 elif [[ $_os == "bsd" || $_os == "macos" ]]; then
144 date -u -j -f "$in_format" "$date" +"$format"
145 else
146 date --date "$date" -u +"$format"
147 fi
148 fi
149}
150
151function atfile.util.get_date_json() {
152 date="$1"
153 parsed="$2"
154
155 if [[ -z "$parsed" ]]; then
156 if [[ -n "$date" ]]; then
157 parsed_date="$(atfile.util.get_date "$date" 2> /dev/null)"
158 # shellcheck disable=SC2181
159 [[ $? == 0 ]] && parsed="$parsed_date"
160 fi
161 fi
162
163 if [[ -n "$parsed" ]]; then
164 echo "\"$parsed\""
165 else
166 echo "null"
167 fi
168}
169
170function atfile.util.get_didplc_doc() {
171 actor="$1"
172
173 function atfile.util.get_didplc_doc.request_doc() {
174 endpoint="$1"
175 actor="$2"
176
177 curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$endpoint/$actor"
178 }
179
180 # shellcheck disable=SC2154
181 didplc_endpoint="$_endpoint_plc_directory"
182 didplc_doc="$(atfile.util.get_didplc_doc.request_doc "$didplc_endpoint" "$actor")"
183
184 if [[ "$didplc_doc" != "{"* ]]; then
185 # shellcheck disable=SC2154
186 didplc_endpoint="https://plc.directory"
187 didplc_doc="$(atfile.util.get_didplc_doc.request_doc "$didplc_endpoint" "$actor")"
188 fi
189
190 echo "$didplc_doc" | jq ". += {\"directory\": \"$didplc_endpoint\"}"
191}
192
193function atfile.util.get_didweb_doc_url() {
194 actor="$1"
195 echo "https://${actor//did:web:/}/.well-known/did.json"
196}
197
198function atfile.util.get_envvar() {
199 envvar="$1"
200 default="$2"
201 envvar_from_envfile="$(atfile.util.get_envvar_from_envfile "$envvar")"
202 envvar_value=""
203
204 if [[ -n "${!envvar}" ]]; then
205 envvar_value="${!envvar}"
206 elif [[ -n "$envvar_from_envfile" ]]; then
207 envvar_value="$envvar_from_envfile"
208 fi
209
210 if [[ -z "$envvar_value" ]]; then
211 envvar_value="$default"
212 fi
213
214 echo "$envvar_value"
215}
216
217function atfile.util.get_envvar_from_envfile() {
218 variable="$1"
219 [[ -f $_path_envvar ]] && atfile.util.get_var_from_file "$_path_envvar" "$variable"
220}
221
222function atfile.util.get_exiftool_field() {
223 file="$1"
224 tag="$2"
225 default="$3"
226 output=""
227
228 exiftool_output="$(eval "exiftool -c \"%+.6f\" -s -T -$tag \"$file\"")"
229
230 if [[ -n "$exiftool_output" ]]; then
231 if [[ "$exiftool_output" == "-" ]]; then
232 output="$default"
233 else
234 output="$exiftool_output"
235 fi
236 else
237 output="$default"
238 fi
239
240 echo "$(echo "$output" | sed "s|\"|\\\\\"|g")"
241}
242
243function atfile.util.get_file_name_pretty() {
244 file_record="$1"
245 emoji="$(atfile.util.get_file_type_emoji "$(echo "$file_record" | jq -r '.file.mimeType')")"
246 file_name_no_ext="$(echo "$file_record" | jq -r ".file.name" | cut -d "." -f 1)"
247 output="$file_name_no_ext"
248
249 meta_type="$(echo "$file_record" | jq -r ".meta.\"\$type\"")"
250
251 if [[ -n "$meta_type" ]]; then
252 case $meta_type in
253 "$_nsid_meta#audio")
254 album="$(echo "$file_record" | jq -r ".meta.tags.album")"
255 album_artist="$(echo "$file_record" | jq -r ".meta.tags.album_artist")"
256 date="$(echo "$file_record" | jq -r ".meta.tags.date")"
257 disc="$(echo "$file_record" | jq -r ".meta.tags.disc.position")"
258 title="$(echo "$file_record" | jq -r ".meta.tags.title")"
259 track="$(echo "$file_record" | jq -r ".meta.tags.track.position")"
260
261 [[ $(atfile.util.is_null_or_empty "$album") == 1 ]] && album="(Unknown Album)"
262 [[ $(atfile.util.is_null_or_empty "$album_artist") == 1 ]] && album_artist="(Unknown Artist)"
263 [[ $(atfile.util.is_null_or_empty "$disc") == 1 ]] && disc=0
264 [[ $(atfile.util.is_null_or_empty "$title") == 1 ]] && title="$file_name_no_ext"
265 [[ $(atfile.util.is_null_or_empty "$track") == 1 ]] && track=0
266
267 output="$title\n $album_artist — $album"
268 [[ $(atfile.util.is_null_or_empty "$date") == 0 ]] && output+=" ($(atfile.util.get_date "$date" "%Y"))"
269 [[ $disc != 0 || $track != 0 ]] && output+=" [$disc.$track]"
270 ;;
271 "$_nsid_meta#photo")
272 date="$(echo "$file_record" | jq -r ".meta.date.create")"
273 lat="$(echo "$file_record" | jq -r ".meta.gps.lat")"
274 long="$(echo "$file_record" | jq -r ".meta.gps.long")"
275 title="$(echo "$file_record" | jq -r ".meta.title")"
276
277 [[ -z "$title" ]] && title="$file_name_no_ext"
278
279 output="$title"
280
281 if [[ $(atfile.util.is_null_or_empty "$lat") == 0 && $(atfile.util.is_null_or_empty "$long") == 0 ]]; then
282 output+="\n $long $lat"
283
284 if [[ $(atfile.util.is_null_or_empty "$date") == 0 ]]; then
285 output+=" — $(atfile.util.get_date "$date")"
286 fi
287 fi
288 ;;
289 "$_nsid_meta#video")
290 title="$(echo "$file_record" | jq -r ".meta.tags.title")"
291
292 [[ $(atfile.util.is_null_or_empty "$title") == 1 ]] && title="$file_name_no_ext"
293
294 output="$title"
295 ;;
296 esac
297 fi
298
299 # BUG: Haiku Terminal has issues with emojis
300 if [[ $_os != "haiku" ]]; then
301 output="$emoji $output"
302 fi
303
304 output_last_line="$(echo -e "$output" | tail -n1)"
305 output_last_line_length="${#output_last_line}"
306
307 echo -e "$output"
308 echo -e "$(atfile.util.repeat_char "-" "$output_last_line_length")"
309}
310
311function atfile.util.get_file_size_pretty() {
312 size="$1"
313 suffix=""
314
315 if (( size >= 1048576 )); then
316 size=$(( size / 1048576 ))
317 suffix="MiB"
318 elif (( size >= 1024 )); then
319 size=$(( size / 1024 ))
320 suffix="KiB"
321 else
322 suffix="B"
323 fi
324
325 echo "$size $suffix"
326}
327
328# NOTE: There is currently no API for getting the filesize limit on the server
329function atfile.util.get_file_size_surplus_for_pds() {
330 size="$1"
331 pds="$2"
332
333 unset max_filesize
334
335 case $pds in
336 *".host.bsky.network") max_filesize=1073741824 ;;
337 esac
338
339 if [[ -z $max_filesize ]] || [[ $max_filesize == 0 ]] || (( size < max_filesize )); then
340 echo 0
341 else
342 echo $(( size - max_filesize ))
343 fi
344}
345
346function atfile.util.get_file_type_emoji() {
347 mime_type="$1"
348 short_type="$(echo "$mime_type" | cut -d "/" -f 1)"
349 desc_type="$(echo "$mime_type" | cut -d "/" -f 2)"
350
351 case $short_type in
352 "application")
353 case "$desc_type" in
354 # Apps (Desktop)
355 "vnd.debian.binary-package"| \
356 "vnd.microsoft.portable-executable"| \
357 "x-executable"| \
358 "x-rpm")
359 echo "💻" ;;
360 # Apps (Mobile)
361 "vnd.android.package-archive"| \
362 "x-ios-app")
363 echo "📱" ;;
364 # Archives
365 "prs.atfile.car"| \
366 "gzip"|"x-7z-compressed"|"x-apple-diskimage"|"x-bzip2"|"x-stuffit"|"x-xz"|"zip")
367 echo "📦" ;;
368 # Disk Images
369 "x-iso9660-image")
370 echo "💿" ;;
371 # Encrypted
372 "prs.atfile.gpg-crypt")
373 echo "🔑" ;;
374 # Rich Text
375 "pdf"| \
376 "vnd.oasis.opendocument.text")
377 echo "📄" ;;
378 *) echo "⚙️ " ;;
379 esac
380 ;;
381 "audio") echo "🎵" ;;
382 "font") echo "✏️" ;;
383 "image") echo "🖼️ " ;;
384 "inode") echo "🔌" ;;
385 "text")
386 case "$mime_type" in
387 "text/x-shellscript") echo "⚙️ " ;;
388 *) echo "📄" ;;
389 esac
390 ;;
391 "video") echo "📼" ;;
392 *) echo "❓" ;;
393 esac
394}
395
396function atfile.util.get_int_suffix() {
397 int="$1"
398 singular="$2"
399 plural="$3"
400
401 [[ $int == 1 ]] && echo -e "$singular" || echo -e "$plural"
402}
403
404function atfile.util.get_finger_record() {
405 fingerprint_override="$1"
406 unset enable_fingerprint_original
407
408 if [[ $fingerprint_override ]]; then
409 enable_fingerprint_original="$_enable_fingerprint"
410 _enable_fingerprint="$fingerprint_override"
411 fi
412
413 echo -e "$(blue.zio.atfile.finger__machine)"
414
415 if [[ -n $enable_fingerprint_original ]]; then
416 _enable_fingerprint="$enable_fingerprint_original"
417 fi
418}
419
420function atfile.util.get_line() {
421 input="$1"
422 index=$(( $2 + 1 ))
423
424 echo -e "$input" | sed -n "$(( index ))"p
425}
426
427function atfile.util.get_mediainfo_field() {
428 file="$1"
429 category="$2"
430 field="$3"
431 default="$4"
432 output=""
433
434 mediainfo_output="$(mediainfo --Inform="$category;%$field%\n" "$file")"
435
436 if [[ -n "$mediainfo_output" ]]; then
437 if [[ "$mediainfo_output" == "None" ]]; then
438 output="$default"
439 else
440 output="$mediainfo_output"
441 fi
442 else
443 output="$default"
444 fi
445
446 echo "$(echo "$output" | sed "s|\"|\\\\\"|g")"
447}
448
449function atfile.util.get_mediainfo_audio_json() {
450 file="$1"
451
452 bitRates=$(atfile.util.get_mediainfo_field "$file" "Audio" "BitRate" 0)
453 bitRate_modes=$(atfile.util.get_mediainfo_field "$file" "Audio" "BitRate_Mode" "")
454 channelss=$(atfile.util.get_mediainfo_field "$file" "Audio" "Channels" 0)
455 compressions="$(atfile.util.get_mediainfo_field "$file" "Audio" "Compression_Mode" "")"
456 durations=$(atfile.util.get_mediainfo_field "$file" "Audio" "Duration" 0)
457 formats="$(atfile.util.get_mediainfo_field "$file" "Audio" "Format" "")"
458 format_ids="$(atfile.util.get_mediainfo_field "$file" "Audio" "CodecID" "")"
459 format_profiles="$(atfile.util.get_mediainfo_field "$file" "Audio" "Format_Profile" "")"
460 samplings=$(atfile.util.get_mediainfo_field "$file" "Audio" "SamplingRate" 0)
461 titles="$(atfile.util.get_mediainfo_field "$file" "Audio" "Title" "")"
462
463 lines="$(echo "$bitRates" | wc -l)"
464 output=""
465
466 for (( i = 0 ; i < lines ; i++ )); do
467 lossy=true
468
469 [[ $(atfile.util.get_line "$compressions" $i) == "Lossless" ]] && lossy=false
470
471 output+="{
472 \"bitRate\": $(atfile.util.get_line "$bitRates" $i),
473 \"channels\": $(atfile.util.get_line "$channelss" $i),
474 \"duration\": $(atfile.util.get_line "$durations" $i),
475 \"format\": {
476 \"id\": \"$(atfile.util.get_line "$format_ids" $i)\",
477 \"name\": \"$(atfile.util.get_line "$formats" $i)\",
478 \"profile\": \"$(atfile.util.get_line "$format_profiles" $i)\"
479 },
480 \"mode\": \"$(atfile.util.get_line "$bitRate_modes" $i)\",
481 \"lossy\": $lossy,
482 \"sampling\": $(atfile.util.get_line "$samplings" $i),
483 \"title\": \"$(atfile.util.get_line "$titles" $i)\"
484},"
485 done
486
487 echo "${output::-1}"
488}
489
490function atfile.util.get_mediainfo_video_json() {
491 file="$1"
492
493 bitRates=$(atfile.util.get_mediainfo_field "$file" "Video" "BitRate" 0)
494 dim_height=$(atfile.util.get_mediainfo_field "$file" "Video" "Height" 0)
495 dim_width=$(atfile.util.get_mediainfo_field "$file" "Video" "Width" 0)
496 durations=$(atfile.util.get_mediainfo_field "$file" "Video" "Duration" 0)
497 formats="$(atfile.util.get_mediainfo_field "$file" "Video" "Format" "")"
498 format_ids="$(atfile.util.get_mediainfo_field "$file" "Video" "CodecID" "")"
499 format_profiles="$(atfile.util.get_mediainfo_field "$file" "Video" "Format_Profile" "")"
500 frameRates="$(atfile.util.get_mediainfo_field "$file" "Video" "FrameRate" "")"
501 frameRate_modes="$(atfile.util.get_mediainfo_field "$file" "Video" "FrameRate_Mode" "")"
502 titles="$(atfile.util.get_mediainfo_field "$file" "Video" "Title" "")"
503
504 lines="$(echo "$bitRates" | wc -l)"
505 output=""
506
507 for ((i = 0 ; i < lines ; i++ )); do
508 output+="{
509 \"bitRate\": $(atfile.util.get_line "$bitRates" $i),
510 \"dimensions\": {
511 \"height\": $dim_height,
512 \"width\": $dim_width
513 },
514 \"duration\": $(atfile.util.get_line "$durations" $i),
515 \"format\": {
516 \"id\": \"$(atfile.util.get_line "$format_ids" $i)\",
517 \"name\": \"$(atfile.util.get_line "$formats" $i)\",
518 \"profile\": \"$(atfile.util.get_line "$format_profiles" $i)\"
519 },
520 \"frameRate\": $(atfile.util.get_line "$frameRates" $i),
521 \"mode\": \"$(atfile.util.get_line "$frameRate_modes" $i)\",
522 \"title\": \"$(atfile.util.get_line "$titles" $i)\"
523},"
524 done
525
526 echo "${output::-1}"
527}
528
529function atfile.util.get_meta_record() {
530 file="$1"
531 type="$2"
532
533 case "$type" in
534 "audio/"*) blue.zio.atfile.meta__audio "$1" ;;
535 "image/"*) blue.zio.atfile.meta__photo "$1" ;;
536 "video/"*) blue.zio.atfile.meta__video "$1" ;;
537 *) blue.zio.atfile.meta__unknown "" "$type" ;;
538 esac
539}
540
541function atfile.util.get_md5() {
542 file="$1"
543
544 unset checksum
545 type="none"
546
547 if [ -x "$(command -v md5sum)" ]; then
548 hash="$(md5sum "$file" | cut -f 1 -d " ")"
549 if [[ ${#hash} == 32 ]]; then
550 checksum="$hash"
551 type="md5"
552 fi
553 fi
554
555 echo "$checksum|$type"
556}
557
558function atfile.util.get_os() {
559 os="${OSTYPE,,}"
560
561 [[ -n $_force_os ]] && os="force-${_force_os,,}"
562
563 # shellcheck disable=SC2221
564 # shellcheck disable=SC2222
565 case $os in
566 # BSD
567 "freebsd"*|"netbsd"*|"openbsd"*|*"bsd"|"force-bsd") echo "bsd" ;;
568 # Haiku
569 "haiku"|"force-haiku") echo "haiku" ;;
570 # Linux
571 "linux-gnu"|"force-linux") echo "linux" ;;
572 "cygwin"|"msys"|"force-linux-mingw") echo "linux-mingw" ;;
573 "linux-musl"|"force-linux-musl") echo "linux-musl" ;;
574 "linux-android"|"force-linux-termux") echo "linux-termux" ;;
575 # macOS
576 "darwin"*|"force-macos") echo "macos" ;;
577 # SerenityOS
578 "serenity"*|"force-serenity") echo "serenity" ;;
579 # Solaris
580 "solaris"*|"force-solaris") echo "solaris" ;;
581 # Unknown
582 *) echo "unknown-${os//force-/}" ;;
583 esac
584}
585
586function atfile.util.get_pds_pretty() {
587 pds="$1"
588
589 pds_host="$(atfile.util.get_uri_segment "$pds" host)"
590 unset pds_name
591 unset pds_emoji
592
593 case "$pds_host" in
594 *".host.bsky.network")
595 bsky_host="$(echo "$pds_host" | cut -d "." -f 1)"
596 bsky_region="$(echo "$pds_host" | cut -d "." -f 2)"
597
598 pds_name="${bsky_host^} ($(atfile.util.get_region_pretty "$bsky_region"))"
599 pds_emoji="🍄"
600 ;;
601 "at.app.wafrn.net") pds_name="Wafrn"; pds_emoji="🌸" ;;
602 "atproto.brid.gy") pds_name="Bridgy Fed"; pds_emoji="🔀" ;;
603 "blacksky.app") pds_name="Blacksky"; pds_emoji="⬛" ;;
604 "pds.sprk.so") pds_name="Spark"; pds_emoji="✨" ;;
605 "tngl.sh") pds_name="Tangled"; pds_emoji="🪢" ;;
606 *)
607 pds_oauth_url="$pds/oauth/authorize"
608 pds_oauth_page="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$pds_oauth_url" | tr -d '\n')"
609 pds_customization_data="$(echo "$pds_oauth_page" | sed -n 's/.*window\["__customizationData"\]=JSON.parse("\(.*\)");.*/\1/p' | sed 's/\\"/"/g; s/\\\\/\\/g' | sed 's/");window\[".*$//')"
610
611 if [[ $pds_customization_data == "{"* ]]; then
612 pds_name="$(echo "$pds_customization_data" | jq -r '.name')"
613 pds_emoji="🟦"
614 else
615 pds_name="$pds_host"
616 fi
617 ;;
618 esac
619
620 # BUG: Haiku Terminal has issues with emojis
621 if [[ -n "$pds_emoji" ]] && [[ $_os != "haiku" ]]; then
622 echo "$pds_emoji $pds_name"
623 else
624 echo "$pds_name"
625 fi
626}
627
628function atfile.util.get_random() {
629 amount="$1"
630 [[ -z "$amount" ]] && amount="6"
631 echo "$(tr -dc A-Za-z0-9 </dev/urandom | head -c "$amount"; echo)"
632}
633
634function atfile.util.get_random_pbc_jetstream() {
635 pbc_jetstreams=(
636 "jetstream1.us-east"
637 "jetstream2.us-east"
638 "jetstream1.us-west"
639 "jetstream2.us-east"
640 )
641
642 pbc_jetstream="${pbc_jetstreams[ $RANDOM % ${#pbc_jetstreams[@]} ]}"
643
644 echo "wss://$pbc_jetstream.bsky.network"
645}
646
647function atfile.util.get_realpath() {
648 path="$1"
649
650 if [[ $_os == "solaris" ]]; then
651 # SEE: https://stackoverflow.com/a/11554895
652 # INVESTIGATE: Use this for every OS?
653 # shellcheck disable=SC2015
654 [ -d "$path" ] && (
655 # shellcheck disable=SC1007
656 cd_path= \cd "$1"
657 /bin/pwd
658 ) || (
659 # shellcheck disable=SC1007
660 cd_path= \cd "$(dirname "$1")" &&
661 printf "%s/%s\n" "$(/bin/pwd)" "$(basename "$1")"
662 )
663 else
664 realpath "$path"
665 fi
666}
667
668function atfile.util.get_region_pretty() {
669 region="$1"
670
671 region_sub="$(echo "$region" | cut -d "-" -f 2)"
672 region="$(echo "$region" | cut -d "-" -f 1)"
673
674 echo "${region^^} ${region_sub^}"
675}
676
677function atfile.util.get_rkey_from_at_uri() {
678 at_uri="$1"
679 echo "$at_uri" | cut -d "/" -f 5
680}
681
682function atfile.util.get_seconds_since_start() {
683 current="$(atfile.util.get_date "" "%s")"
684 # shellcheck disable=SC2154
685 echo "$(( current - _start ))"
686}
687
688function atfile.util.get_term_cols() {
689 unset rows
690
691 if [ -x "$(command -v tput)" ]; then
692 cols=$(tput cols)
693 fi
694
695 if [[ -n $cols ]]; then
696 echo "$cols"
697 else
698 echo 80
699 fi
700}
701
702function atfile.util.get_term_rows() {
703 unset rows
704
705 if [ -x "$(command -v tput)" ]; then
706 rows=$(tput lines)
707 fi
708
709 if [[ -n $rows ]]; then
710 echo "$rows"
711 else
712 echo 30
713 fi
714}
715
716function atfile.util.get_var_from_file() {
717 file="$1"
718
719 if [[ -f "$file" ]]; then
720 variable="$2"
721 found_line="$(grep "\b${variable}=" "$file")"
722
723 if [[ -n "$found_line" ]] && [[ ! "$found_line" == \#* ]]; then
724 output="${found_line#"${variable}"=}"
725 output="${output%\"}"
726 output="${output#\"}"
727
728 if [[ $output == *"\$("* && $output == *")"* ]]; then
729 eval "echo \"$output\""
730 else
731 echo "$output"
732 fi
733 fi
734 fi
735}
736
737function atfile.util.get_uas() {
738 # shellcheck disable=SC2154
739 echo "ATFile/$_version"
740}
741
742function atfile.util.get_xrpc_error() {
743 exit_code="$1"
744 data="$2"
745
746 if [[ $exit_code != 0 || -z "$data" || "$data" == "null" || "$data" == "{}" || "$data" == *"\"error\":"* ]]; then
747 if [[ "$data" == "{"* && "$data" == *"\"error\":"* ]]; then
748 error="$(echo "$data" | jq -r ".error")"
749 message="$(echo "$data" | jq -r ".message")"
750
751 if [[ -z $message ]]; then
752 echo "$error"
753 else
754 echo "[$error] $message"
755 fi
756 else
757 echo "?"
758 fi
759 else
760 echo ""
761 fi
762}
763
764function atfile.util.get_yn() {
765 yn="$1"
766
767 # shellcheck disable=SC2154
768 if [[ $_output_json == 0 ]]; then
769 if [[ $yn == 0 ]]; then
770 echo "No"
771 else
772 echo "Yes"
773 fi
774 else
775 if [[ $yn == 0 ]]; then
776 echo "false"
777 else
778 echo "true"
779 fi
780 fi
781}
782
783function atfile.util.is_null_or_empty() {
784 if [[ -z "$1" ]] || [[ "$1" == null ]]; then
785 echo 1
786 else
787 echo 0
788 fi
789}
790
791function atfile.util.is_url_accessible_in_browser() {
792 url="$1"
793 # shellcheck disable=SC2154
794 atfile.util.is_url_okay "$url" "$_test_desktop_uas"
795}
796
797function atfile.util.is_url_okay() {
798 url="$1"
799 uas="$2"
800
801 [[ -z "$uas" ]] && uas="$(atfile.util.get_uas)"
802
803 code="$(curl -H "User-Agent: $uas" -s -o /dev/null -w "%{http_code}" "$url")"
804
805 if [[ "$code" == 2* || "$code" == 3* ]]; then
806 echo 1
807 else
808 echo 0
809 fi
810}
811
812function atfile.util.launch_uri() {
813 uri="$1"
814
815 if [[ -n $DISPLAY ]] && [ -x "$(command -v xdg-open)" ]; then
816 xdg-open "$uri"
817 else
818 case $_os in
819 "haiku") open "$uri" ;;
820 "macos") open "$uri" ;;
821 *) echo "$uri"
822 esac
823 fi
824}
825
826function atfile.util.parse_at_uri() {
827 at_uri="$1"
828 segment="$2"
829 unset parsed_at_uri
830
831 case $segment in
832 "actor"|"did"|"handle") echo "$at_uri" | cut -d "/" -f 3 ;;
833 "collection") echo "$at_uri" | cut -d "/" -f 4 ;;
834 "key"|"rkey") echo "$at_uri" | cut -d "/" -f 5 ;;
835 *) echo "$at_uri" | cut -d "/" -f "$segment" ;;
836 esac
837}
838
839function atfile.util.get_uri_segment() {
840 uri="$1"
841 segment="$2"
842 unset parsed_uri
843
844 case $segment in
845 "host") echo "$uri" | cut -d "/" -f 3 ;;
846 "protocol") echo "$uri" | cut -d ":" -f 1 ;;
847 *) echo "$uri" | cut -d "/" -f "$segment" ;;
848 esac
849}
850
851# HACK: This essentially breaks the entire session (it overrides $_username and
852# $_server). If sourcing, use atfile.util.override_actor_reset() to
853# reset
854function atfile.util.override_actor() {
855 actor="$1"
856
857 [[ -z "$_server_original" ]] && _server_original="$_server"
858 [[ -z "$_username_original" ]] && _username_original="$_username"
859 [[ -z "$_fmt_blob_url_original" ]] && _fmt_blob_url_original="$_fmt_blob_url"
860
861 resolved_did="$(atfile.util.resolve_identity "$actor")"
862 error="$(atfile.util.get_xrpc_error $? "$resolved_did")"
863 [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to resolve '$actor'" "$resolved_did"
864
865 _username="$(echo "$resolved_did" | cut -d "|" -f 1)"
866 _server="$(echo "$resolved_did" | cut -d "|" -f 2)"
867 _fmt_blob_url="$_fmt_blob_url_default"
868
869 atfile.say.debug "Overridden identity\n↳ DID: $_username\n↳ PDS: $_server\n↳ Blob URL: $_fmt_blob_url"
870}
871
872# NOTE: This is to help during sourcing if atfile.uitl.override_actor() has
873# been called
874function atfile.util.override_actor_reset() {
875 [[ -n "$_server_original" ]] && _server="$_server_original"; unset _server_original
876 [[ -n "$_username_original" ]] && _username="$_username_original"; unset _username_original
877 [[ -n "$_fmt_blob_url_original" ]] && _fmt_blob_url="$_fmt_blob_url_original"; unset _fmt_blob_url_original
878}
879
880function atfile.util.parse_exiftool_date() {
881 in_date="$1"
882 tz="$2"
883
884 date="$(echo "$in_date" | cut -d " " -f 1 | sed -e "s|:|-|g")"
885 time="$(echo "$in_date" | cut -d " " -f 2)"
886
887 echo "$date $time $tz"
888}
889
890function atfile.util.parse_version() {
891 version="$1"
892 version="$(echo "$version" | cut -d "+" -f 1)"
893 v_major="$(printf "%04d\n" "$(echo "$version" | cut -d "." -f 1)")"
894 v_minor="$(printf "%04d\n" "$(echo "$version" | cut -d "." -f 2)")"
895 v_rev="$(printf "%04d\n" "$(echo "$version" | cut -d "." -f 3)")"
896 echo "${v_major}${v_minor}${v_rev}" | sed 's/^0*//'
897}
898
899function atfile.util.print_blob_url_output() {
900 blob_uri="$1"
901
902 run_cmd="$_prog url $key"
903 [[ -n "$_username_original" ]] && run_cmd+=" $_username"
904
905 if [[ $(atfile.util.is_url_accessible_in_browser "$blob_uri") == 0 ]]; then
906 echo -e "↳ Blob: ⚠️ Blob cannot be viewed in a browser\n Run '$run_cmd' to get URL"
907 else
908 echo -e "↳ Blob: $blob_uri"
909 fi
910}
911
912function atfile.util.print_override_envvar_debug() {
913 pretty_name="$1"
914 override_variable="$2"
915
916 override_value="$(eval echo "\$_force${override_variable}")"
917 envvar_suffix="_FORCE${override_variable^^}"
918
919 atfile.say.debug "Overriding $pretty_name (\$$override_variable)\n↳ ${_envvar_prefix}$envvar_suffix set to '$override_value'"
920}
921
922function atfile.util.print_seconds_since_start_debug() {
923 seconds=$(atfile.util.get_seconds_since_start)
924 second_unit="$(atfile.util.get_int_suffix "$seconds" "second" "seconds")"
925
926 atfile.say.debug "$seconds $second_unit since start"
927}
928
929function atfile.util.print_table_paginate_hint() {
930 cursor="$1"
931 count="$2"
932
933 # shellcheck disable=SC2154
934 if [[ -z $count ]] || (( ( count + _max_list_buffer ) >= _max_list )); then
935 first_line="List is limited to $_max_list results. To print more results,"
936 first_line_length=$(( ${#first_line} + 3 ))
937 # shellcheck disable=SC2154
938 echo -e "$(atfile.util.repeat_char "-" $first_line_length)\nℹ️ $first_line\n run \`$_prog $_command $cursor\`"
939 fi
940}
941
942function atfile.util.repeat_char() {
943 char="$1"
944 amount="$2"
945
946 if [ -x "$(command -v seq)" ]; then
947 printf "%0.s$char" $(seq 1 "$amount")
948 else
949 echo "$char"
950 fi
951}
952
953function atfile.util.resolve_identity() {
954 actor="$1"
955
956 if [[ "$actor" != "did:"* ]]; then
957 # shellcheck disable=SC2154
958 resolved_handle="$(atfile.xrpc.bsky.get "com.atproto.identity.resolveHandle" "handle=$actor")"
959 error="$(atfile.util.get_xrpc_error $? "$resolved_handle")"
960
961 if [[ -z "$error" ]]; then
962 actor="$(echo "$resolved_handle" | jq -r ".did")"
963 fi
964 fi
965
966 if [[ "$actor" == "did:"* ]]; then
967 unset did_doc
968
969 case "$actor" in
970 "did:plc:"*) did_doc="$(atfile.util.get_didplc_doc "$actor")" ;;
971 "did:web:"*) did_doc="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$(atfile.util.get_didweb_doc_url "$actor")")" ;;
972 *) echo "Unknown DID type 'did:$(echo "$actor" | cut -d ":" -f 2)'"; exit 255;;
973 esac
974
975 if [[ -n "$did_doc" ]]; then
976 did="$(echo "$did_doc" | jq -r ".id")"
977
978 if [[ $(atfile.util.is_null_or_empty "$did") == 1 ]]; then
979 echo "$error"
980 exit 255
981 fi
982
983 unset aliases
984 unset handle
985 didplc_dir="$(echo "$did_doc" | jq -r ".directory")"
986 pds="$(echo "$did_doc" | jq -r '.service[] | select(.id == "#atproto_pds") | .serviceEndpoint')"
987
988 while IFS=$'\n' read -r a; do
989 aliases+="$a;"
990
991 if [[ -z $handle && "$a" == "at://"* && "$a" != "at://did:"* ]]; then
992 handle="$a"
993 fi
994 done <<< "$(echo "$did_doc" | jq -r '.alsoKnownAs[]')"
995
996 [[ $didplc_dir == "null" ]] && unset didplc_dir
997 [[ -z "$handle" ]] && handle="invalid.handle"
998
999 echo "$did|$pds|$handle|$didplc_dir|$aliases"
1000 fi
1001 else
1002 echo "$error"
1003 exit 255
1004 fi
1005}
1006
1007function atfile.util.source_hook() {
1008 file="$1"
1009
1010 if [[ -n "$file" ]]; then
1011 atfile.say.debug "Sourcing: $file"
1012 if [[ -f "$file" ]]; then
1013 # shellcheck disable=SC1090
1014 . "$file"
1015 else
1016 atfile.die "Unable to source '$file'"
1017 fi
1018 fi
1019}
1020
1021function atfile.util.write_cache() {
1022 file="$1"
1023 file_path="$_path_cache/$1"
1024 content="$2"
1025
1026 atfile.util.get_cache "$file"
1027
1028 echo -e "$content" > "$file_path"
1029 # shellcheck disable=SC2320
1030 # shellcheck disable=SC2181
1031 [[ $? != 0 ]] && atfile.die "Unable to write to cache file ($file)"
1032}