├── .gitignore ├── .gitmodules ├── atfile.code-workspace ├── src ├── commands │ ├── token.sh │ ├── now.sh │ ├── ai.sh │ ├── stream.sh │ ├── resolve.sh │ ├── auth.sh │ ├── record.sh │ ├── install.sh │ ├── bsky_profile.sh │ ├── update.sh │ ├── something_broke.sh │ ├── release.sh │ ├── help.sh │ └── old_cmds.sh ├── lexi │ ├── app_bsky.sh │ ├── com_atproto.sh │ └── blue_zio_atfile.sh ├── shared │ ├── cache.sh │ ├── die.sh │ ├── http.sh │ ├── xrpc.sh │ ├── say.sh │ ├── invoke.sh │ └── util.sh └── entry.sh ├── .editorconfig ├── examples └── example.sh ├── .tangled └── workflows │ └── build.yaml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── atfile.sh └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .test_files/* 2 | *~ 3 | bin/* 4 | examples/.gitignore/* 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lexicons"] 2 | path = lexicons 3 | url = https://github.com/ziodotsh/lexicons 4 | -------------------------------------------------------------------------------- /atfile.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /src/commands/token.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.invoke.token() { 4 | atfile.xrpc.pds.jwt 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/now.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.now() { 4 | date="$1" 5 | atfile.util.get_date "$date" 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | 9 | [lexicons/**.json] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /examples/example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | _atfile_path="$(dirname "$(realpath "$0")")/../atfile.sh" 4 | 5 | if [[ ! -f "$_atfile_path" ]]; then 6 | echo -e "\033[1;31mError: ATFile not found (download: https://zio.sh/atfile)\033[0m" 7 | exit 0 8 | fi 9 | 10 | # shellcheck disable=SC1090 11 | source "$_atfile_path" 12 | 13 | atfile.bsky_profile "ducky.ws" 14 | -------------------------------------------------------------------------------- /src/lexi/app_bsky.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # app.bsky.* 4 | 5 | ## Queries 6 | 7 | function app.bsky.actor.getProfile() { 8 | actor="$1" 9 | atfile.xrpc.bsky.get "app.bsky.actor.getProfile" "actor=$actor" "" "$appview" 10 | } 11 | 12 | function app.bsky.labeler.getServices() { 13 | did="$1" 14 | atfile.xrpc.bsky.get "app.bsky.labeler.getServices" "dids=$did" 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/ai.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.ai() { 4 | ai_art_record="$(com.atproto.repo.getRecord "did:plc:ragtjsm2j2vknwkz3zp4oxrd" "app.bsky.feed.post" "3jj2zikhvto2h")" 5 | error="$(atfile.util.get_xrpc_error $? "$ai_art_record")" 6 | 7 | if [[ -z "$error" ]]; then 8 | atfile.say "$(echo "$ai_art_record" | jq -r .value.text)" 9 | else 10 | atfile.say ":(" 11 | fi 12 | } 13 | -------------------------------------------------------------------------------- /.tangled/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: ["push", "manual"] 3 | branch: ["main", "dev"] 4 | - event: ["pull_request"] 5 | branch: ["dev"] 6 | 7 | engine: "nixery" 8 | 9 | clone: 10 | skip: false 11 | depth: 1 12 | submodules: false 13 | 14 | dependencies: 15 | nixpkgs: 16 | - bash 17 | - shellcheck 18 | 19 | environment: 20 | ATFILE_DEVEL_ENABLE_PUBLISH: "0" 21 | 22 | steps: 23 | - name: "Build" 24 | command: "./atfile.sh build" 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "bashdb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "atfile.sh" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "appview", 4 | "atfile", 5 | "bsky", 6 | "choo", 7 | "devel", 8 | "didplc", 9 | "exiftool", 10 | "feedgens", 11 | "jetstream", 12 | "jetstreams", 13 | "lexi", 14 | "mediainfo", 15 | "nsid", 16 | "pkgman", 17 | "rkey", 18 | "termux", 19 | "urandom", 20 | "websocat", 21 | "xrpc" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/cache.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.cache.del() { 4 | key="$(atfile.util.get_cache_path "$1")" 5 | [[ -f "$key" ]] && rm "$key" 6 | } 7 | 8 | function atfile.cache.get() { 9 | key="$(atfile.util.get_cache_path "$1")" 10 | [[ -f "$key" ]] && cat "$key" 11 | } 12 | 13 | function atfile.cache.set() { 14 | key="$(atfile.util.get_cache_path "$1")" 15 | value="$2" 16 | 17 | # shellcheck disable=SC2154 18 | mkdir -p "$_path_cache" 19 | echo "$value" > "$key" 20 | echo "$value" 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/stream.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.stream() { 4 | collection="$1" 5 | did="$2" 6 | cursor="$3" 7 | compress="$4" 8 | 9 | atfile.util.check_prog "websocat" "https://github.com/vi/websocat" 10 | 11 | [[ "$compress" == 1 ]] && compress="true" 12 | 13 | # shellcheck disable=SC2154 14 | atfile.say.debug "Streaming: $_endpoint_jetstream\n↳ Collection: $(echo "$collection" | sed -s 's/;/, /g')\n↳ DID: $(echo "$did" | sed -s 's/;/, /g')\n↳ Cursor: $cursor\n↳ Compress: $compress" 15 | 16 | collection_query="$(atfile.util.build_query_array "wantedCollections" "$collection")" 17 | did_query="$(atfile.util.build_query_array "wantedDids" "$did")" 18 | cursor_query="$([[ -n "$cursor" ]] && echo "cursor=$cursor&" || echo "cursor=$(atfile.util.get_date "$_now" "%s")")" 19 | compress_query="$([[ -n "$compress" ]] && echo "compress=$compress&")" 20 | 21 | url="$_endpoint_jetstream/subscribe?${collection_query}${did_query}${cursor_query}${compress_query}" 22 | url="${url::-1}" 23 | 24 | atfile.say.debug "Using URL '$url'" 25 | 26 | websocat "${url}" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 zio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/shared/die.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.die() { 4 | message="$1" 5 | 6 | if [[ $_output_json != 1 ]]; then 7 | atfile.say.die "$message" 8 | else 9 | echo -e "{ \"error\": \"$1\" }" | jq 10 | fi 11 | 12 | [[ $_is_sourced == 0 ]] && exit 255 13 | } 14 | 15 | function atfile.die.gui() { 16 | cli_error="$1" 17 | gui_error="$2" 18 | 19 | [[ -z "$gui_error" ]] && gui_error="$cli_error" 20 | 21 | if [ -x "$(command -v zenity)" ] && [[ $_is_sourced == 0 ]]; then 22 | zenity --error --text "$gui_error" 23 | fi 24 | 25 | atfile.die "$cli_error" 26 | } 27 | 28 | function atfile.die.gui.xrpc_error() { 29 | message="$1" 30 | xrpc_error="$2" 31 | message_cli="$message" 32 | 33 | [[ "$xrpc_error" == "?" ]] && unset xrpc_error 34 | [[ -n "$xrpc_error" ]] && message_cli="$message\n↳ $xrpc_error" 35 | 36 | atfile.die.gui \ 37 | "$message_cli" \ 38 | "$message" 39 | } 40 | 41 | function atfile.die.xrpc_error() { 42 | message="$1" 43 | xrpc_error="$2" 44 | 45 | [[ "$xrpc_error" == "?" ]] && unset xrpc_error 46 | [[ -n "$xrpc_error" && "$xrpc_error" != "{}" ]] && message="$message\n↳ $xrpc_error" 47 | 48 | atfile.die "$message" 49 | } 50 | 51 | function atfile.die.unknown_command() { 52 | command="$1" 53 | atfile.die "Unknown command '$1'" 54 | } 55 | -------------------------------------------------------------------------------- /src/shared/http.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.http.download() { 4 | uri="$1" 5 | out_path="$2" 6 | 7 | atfile.say.debug "$uri\n↳ $out_path" "GET: " 8 | 9 | curl -s -X GET "$uri" \ 10 | -H "User-Agent: $(atfile.util.get_uas)" \ 11 | -o "$out_path" 12 | } 13 | 14 | function atfile.http.get() { 15 | uri="$1" 16 | auth="$2" 17 | type="$3" 18 | 19 | [[ -n $auth ]] && auth="Authorization: $auth" 20 | [[ -z $type ]] && type="application/json" 21 | 22 | atfile.say.debug "$uri" "GET: " 23 | 24 | curl -s -X GET "$uri" \ 25 | -H "$auth" \ 26 | -H "Content-Type: $type" \ 27 | -H "User-Agent: $(atfile.util.get_uas)" 28 | } 29 | 30 | function atfile.http.post() { 31 | uri="$1" 32 | data="$2" 33 | auth="$3" 34 | type="$4" 35 | 36 | [[ -n $auth ]] && auth="Authorization: $auth" 37 | [[ -z $type ]] && type="application/json" 38 | 39 | atfile.say.debug "$uri\n↳ $data" "POST: " 40 | 41 | curl -s -X POST "$uri" \ 42 | -H "$auth" \ 43 | -H "Content-Type: $type" \ 44 | -H "User-Agent: $(atfile.util.get_uas)" \ 45 | -d "$data" 46 | } 47 | 48 | function atfile.http.upload() { 49 | uri="$1" 50 | file="$2" 51 | auth="$3" 52 | type="$4" 53 | 54 | [[ -n $auth ]] && auth="Authorization: $auth" 55 | [[ -z $type ]] && type="*/*" 56 | 57 | atfile.say.debug "$uri\n↳ $file" "POST: " 58 | 59 | # shellcheck disable=SC2154 60 | curl -s -X POST "$_server/xrpc/$lexi" \ 61 | -H "$auth" \ 62 | -H "Content-Type: $type" \ 63 | -H "User-Agent: $(atfile.util.get_uas)" \ 64 | --data-binary @"$file" | jq 65 | } 66 | -------------------------------------------------------------------------------- /src/shared/xrpc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # PDS 4 | 5 | function atfile.xrpc.pds.blob() { 6 | file="$1" 7 | type="$2" 8 | lexi="$3" 9 | 10 | [[ -z $lexi ]] && lexi="com.atproto.repo.uploadBlob" 11 | [[ -z $type ]] && type="*/*" 12 | 13 | atfile.http.upload \ 14 | "$_server/xrpc/$lexi" \ 15 | "$file" \ 16 | "Bearer $(atfile.xrpc.pds.jwt)" \ 17 | "$type" | jq 18 | } 19 | 20 | function atfile.xrpc.pds.get() { 21 | lexi="$1" 22 | query="$2" 23 | type="$3" 24 | endpoint="$4" 25 | 26 | [[ -z $endpoint ]] && endpoint="$_server" 27 | 28 | atfile.http.get \ 29 | "$endpoint/xrpc/$lexi?$query" \ 30 | "Bearer $(atfile.xrpc.pds.jwt)" \ 31 | "$type" | jq 32 | } 33 | 34 | function atfile.xrpc.pds.jwt() { 35 | atfile.http.post \ 36 | "$_server/xrpc/com.atproto.server.createSession" \ 37 | '{"identifier": "'"$_username"'", "password": "'"$_password"'"}' | jq -r ".accessJwt" 38 | } 39 | 40 | function atfile.xrpc.pds.post() { 41 | lexi="$1" 42 | data="$2" 43 | type="$3" 44 | 45 | [[ -z $type ]] && type="application/json" 46 | 47 | curl -s -X POST "$_server/xrpc/$lexi" \ 48 | -H "Authorization: Bearer $(atfile.xrpc.pds.jwt)" \ 49 | -H "Content-Type: $type" \ 50 | -H "User-Agent: $(atfile.util.get_uas)" \ 51 | -d "$data" | jq 52 | } 53 | 54 | # AppView 55 | 56 | ## Bluesky 57 | 58 | function atfile.xrpc.bsky.get() { 59 | lexi="$1" 60 | query="$2" 61 | type="$3" 62 | appview="$4" 63 | 64 | # shellcheck disable=SC2154 65 | [[ -z "$appview" ]] && appview="$_endpoint_appview" 66 | 67 | atfile.http.get \ 68 | "$appview/xrpc/$lexi?$query" \ 69 | "" \ 70 | "$type" | jq 71 | } 72 | -------------------------------------------------------------------------------- /src/shared/say.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.say() { 4 | message="$1" 5 | prefix="$2" 6 | color_prefix="$3" 7 | color_message="$4" 8 | color_prefix_message="$5" 9 | suffix="$6" 10 | 11 | prefix_length=0 12 | 13 | # shellcheck disable=SC2154 14 | if [[ $_os == "haiku" ]]; then 15 | message="${message//↳/>}" 16 | fi 17 | 18 | [[ -z $color_prefix_message ]] && color_prefix_message=0 19 | [[ -z $suffix ]] && suffix="\n" 20 | [[ $suffix == "\\" ]] && suffix="" 21 | 22 | if [[ -z $color_message ]]; then 23 | color_message="\033[0m" 24 | else 25 | color_message="\033[${color_prefix_message};${color_message}m" 26 | fi 27 | 28 | if [[ -z $color_prefix ]]; then 29 | color_prefix="\033[0m" 30 | else 31 | color_prefix="\033[1;${color_prefix}m" 32 | fi 33 | 34 | if [[ -n $prefix ]]; then 35 | if [[ $prefix == *":"* ]]; then 36 | prefix_length=${#prefix} 37 | prefix="${color_prefix}${prefix}\033[0m" 38 | else 39 | prefix_length=$(( ${#prefix} + 2 )) 40 | prefix="${color_prefix}${prefix}: \033[0m" 41 | fi 42 | fi 43 | 44 | message="${message//\\n/\\n$(atfile.util.repeat_char " " "$prefix_length")}" 45 | 46 | echo -n -e "${prefix}${color_message}$message\033[0m${suffix}" 47 | } 48 | 49 | function atfile.say.debug() { 50 | message="$1" 51 | prefix="$2" 52 | 53 | [[ -z "$prefix" ]] && prefix="Debug" 54 | 55 | if [[ $_debug == 1 ]]; then 56 | atfile.say "$message" "$prefix" 35 >&2 57 | fi 58 | } 59 | 60 | function atfile.say.die() { 61 | message="$1" 62 | atfile.say "$message" "Error" 31 31 1 >&2 63 | } 64 | 65 | function atfile.say.inline() { 66 | message="$1" 67 | color="$2" 68 | atfile.say "$message" "" "" "$color" "" "\\" 69 | } 70 | -------------------------------------------------------------------------------- /src/lexi/com_atproto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # com.atproto.* 4 | 5 | ## Queries 6 | 7 | function com.atproto.repo.createRecord() { 8 | repo="$1" 9 | collection="$2" 10 | record="$3" 11 | 12 | atfile.xrpc.pds.post "com.atproto.repo.createRecord" "{\"repo\": \"$repo\", \"collection\": \"$collection\", \"record\": $record }" 13 | } 14 | 15 | function com.atproto.repo.deleteRecord() { 16 | repo="$1" 17 | collection="$2" 18 | rkey="$3" 19 | 20 | atfile.xrpc.pds.post "com.atproto.repo.deleteRecord" "{ \"repo\": \"$repo\", \"collection\": \"$collection\", \"rkey\": \"$rkey\" }" 21 | } 22 | 23 | function com.atproto.repo.getRecord() { 24 | repo="$1" 25 | collection="$2" 26 | key="$3" 27 | 28 | atfile.xrpc.pds.get "com.atproto.repo.getRecord" "repo=$repo&collection=$collection&rkey=$key" 29 | } 30 | 31 | function com.atproto.repo.listRecords() { 32 | repo="$1" 33 | collection="$2" 34 | cursor="$3" 35 | 36 | query="repo=$repo&collection=$collection&limit=$_max_list" 37 | [[ -n "$cursor" ]] && query+="&cursor=$cursor" 38 | 39 | atfile.xrpc.pds.get "com.atproto.repo.listRecords" "$query" 40 | } 41 | 42 | function com.atproto.repo.putRecord() { 43 | repo="$1" 44 | collection="$2" 45 | rkey="$3" 46 | record="$4" 47 | 48 | atfile.xrpc.pds.post "com.atproto.repo.putRecord" "{\"repo\": \"$repo\", \"collection\": \"$collection\", \"rkey\": \"$rkey\", \"record\": $record }" 49 | } 50 | 51 | function com.atproto.identity.resolveHandle() { 52 | handle="$1" 53 | 54 | atfile.xrpc.pds.get "com.atproto.identity.resolveHandle" "handle=$handle" 55 | } 56 | 57 | function com.atproto.server.getSession() { 58 | atfile.xrpc.pds.get "com.atproto.server.getSession" 59 | } 60 | 61 | function com.atproto.sync.getBlob() { 62 | did="$1" 63 | cid="$2" 64 | 65 | atfile.xrpc.pds.get "com.atproto.sync.getBlob" "cid=$cid&did=$did" "*/*" 66 | } 67 | 68 | function com.atproto.sync.listBlobs() { 69 | did="$1" 70 | cursor="$2" 71 | 72 | query="did=$did&limit=$_max_list" 73 | [[ -n "$cursor" ]] && query+="&cursor=$cursor" 74 | 75 | atfile.xrpc.pds.get "com.atproto.sync.listBlobs" "$query" 76 | } 77 | 78 | function com.atproto.sync.uploadBlob() { 79 | file="$1" 80 | atfile.xrpc.pds.blob "$1" | jq -r ".blob" 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/resolve.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.resolve() { 4 | actor="$1" 5 | 6 | [[ -z $actor && -n $_username ]] && actor="$_username" 7 | 8 | atfile.say.debug "Resolving actor '$actor'..." 9 | 10 | resolved_did="$(atfile.util.resolve_identity "$actor")" 11 | error="$(atfile.util.get_xrpc_error $? "$resolved_did")" 12 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to resolve '$actor'" "$resolved_did" 13 | 14 | aliases="$(echo "$resolved_did" | cut -d "|" -f 5)" 15 | did="$(echo "$resolved_did" | cut -d "|" -f 1)" 16 | did_doc="$(echo "$resolved_did" | cut -d "|" -f 4)/$did" 17 | did_type="did:$(echo "$did" | cut -d ":" -f 2)" 18 | handle="$(echo "$resolved_did" | cut -d "|" -f 3 | cut -d "/" -f 3)" 19 | pds="$(echo "$resolved_did" | cut -d "|" -f 2)" 20 | pds_name="$(atfile.util.get_pds_pretty "$pds")" 21 | pds_software="Bluesky PDS" 22 | atfile.say.debug "Getting PDS version for '$pds'..." 23 | pds_version="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -l -X GET "$pds/xrpc/_health" | jq -r '.version')" 24 | 25 | if [[ "$pds_version" == *" v"* ]]; then 26 | pds_software="🐌 $(echo "$pds_version" | sed -s 's/ v/\n/g' | sed -n 1p)" 27 | pds_version="$(echo "$pds_version" | sed -s 's/ v/\n/g' | sed -n 2p)" 28 | 29 | if [[ "$pds_name" == "$(atfile.util.get_uri_segment "$pds" host)" ]]; then 30 | pds_name="$pds_software" 31 | fi 32 | fi 33 | 34 | case "$did_type" in 35 | "did:web") 36 | did_doc="$(atfile.util.get_didweb_doc_url "$did")" 37 | ;; 38 | esac 39 | 40 | # shellcheck disable=SC2154 41 | if [[ $_output_json == 1 ]]; then 42 | did_doc_data="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -l -X GET "$did_doc")" 43 | aliases_json="$(echo "$did_doc_data" | jq -r ".alsoKnownAs")" 44 | 45 | echo -e "{ 46 | \"aka\": $aliases_json, 47 | \"did\": \"$did\", 48 | \"doc\": { 49 | \"data\": $did_doc_data, 50 | \"url\": \"$did_doc\" 51 | }, 52 | \"handle\": \"$handle\", 53 | \"pds\": { 54 | \"endpoint\": \"$pds\", 55 | \"name\": \"$pds_name\", 56 | \"software\": { 57 | \"name\": \"$pds_software\", 58 | \"version\": \"$pds_version\" 59 | } 60 | }, 61 | \"type\": \"$did_type\" 62 | }" | jq 63 | else 64 | atfile.say "$did" 65 | atfile.say "↳ Type: $did_type" 66 | atfile.say " ↳ Doc: $did_doc" 67 | atfile.say "↳ Handle: @$handle" 68 | 69 | while IFS=$";" read -ra a; do 70 | unset first_alias 71 | 72 | for i in "${a[@]}"; do 73 | if [[ -z "$first_alias" ]]; then 74 | atfile.say " ↳ $i" 75 | else 76 | atfile.say " $i" 77 | fi 78 | 79 | first_alias="${a[0]}" 80 | done 81 | done <<< "$aliases" 82 | 83 | atfile.say "↳ PDS: $pds_name" 84 | atfile.say " ↳ Endpoint: $pds" 85 | [[ $(atfile.util.is_null_or_empty "$pds_version") == 0 ]] && atfile.say " ↳ Version: $pds_version" 86 | fi 87 | } 88 | -------------------------------------------------------------------------------- /atfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # _ _____ _____ _ _ 4 | # / \|_ _| ___(_| | ___ 5 | # / _ \ | | | |_ | | |/ _ \ 6 | # / ___ \| | | _| | | | __/ 7 | # /_/ \_|_| |_| |_|_|\___| 8 | # 9 | # ------------------------------------------------------------------------------- 10 | # 11 | # Welcome to ATFile's crazy Bash codebase! 12 | # 13 | # Unless you're wanting to tinker, its recommended you install a stable version 14 | # of ATFile: see README for more. Using a development version against your 15 | # ATProto account could potentially inadvertently damage records. 16 | # 17 | # Just as a published build, ATFile can be used entirely via this file. The 18 | # below code automatically sources everything for you, and your config (if 19 | # it exists) is utilized as normal. Try running `./atfile.sh help`. To turn 20 | # debug messages off, set ATFILE_DEBUG to '0'. 21 | # 22 | # To produce a single-file build of ATFile, run `./atfile.sh build`: the 23 | # resulting file will be created at './bin/atfile-$version.sh'. 24 | # See README.md ➔ '🏗️ Building' for more details. 25 | # 26 | # Being a fairly atypical codebase, please don't hesitate to get in touch if 27 | # you're wanting to contribute but bewildered by this hot mess. Message 28 | # either @zio.sh or @ducky.ws on Bluesky for help. 29 | # 30 | # Here be dragons. 31 | # 32 | # ------------------------------------------------------------------------------- 33 | 34 | # Meta 35 | 36 | author="zio" 37 | did="did:web:zio.sh" 38 | repo="https://tangled.sh/@zio.sh/atfile" 39 | version="0.12.2" 40 | year="$(date +%Y)" 41 | 42 | # Entry 43 | 44 | function atfile.devel.die() { 45 | echo -e "\033[1;31mError: $1\033[0m" >&2 46 | exit 255 47 | } 48 | 49 | unset ATFILE_DEVEL 50 | unset ATFILE_DEVEL_DIR 51 | unset ATFILE_DEVEL_ENTRY 52 | unset ATFILE_DEVEL_SOURCE 53 | [[ -z "$ATFILE_DEVEL_ENABLE_PIPING" ]] && ATFILE_DEVEL_ENABLE_PIPING=0 54 | 55 | if [[ $ATFILE_DEVEL_ENABLE_PIPING != 1 ]]; then 56 | if [ -p /dev/stdin ] ||\ 57 | [[ "$0" == "bash" || $0 == *"/bin/bash" ]]; then 58 | atfile.devel.die "Piping is disabled\n ↳ Set ATFILE_DEVEL_ENABLE_PIPING=1 to ignore" 59 | fi 60 | fi 61 | 62 | if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then 63 | ATFILE_DEVEL=1 64 | ATFILE_DEVEL_DIR="$(dirname "${BASH_SOURCE[0]}")" 65 | ATFILE_DEVEL_ENTRY="$(realpath "${BASH_SOURCE[0]}")" 66 | # shellcheck disable=SC2034 67 | ATFILE_DEVEL_SOURCE="$ATFILE_DEVEL_ENTRY" 68 | else 69 | # shellcheck disable=SC2034 70 | ATFILE_DEVEL=1 71 | ATFILE_DEVEL_DIR="$(dirname "$(realpath "$0")")" 72 | ATFILE_DEVEL_ENTRY="$(realpath "$0")" 73 | fi 74 | 75 | if [ ! -x "$(command -v git)" ]; then 76 | atfile.devel.die "'git' not installed (download: https://git-scm.com/downloads)" 77 | fi 78 | 79 | git describe --exact-match --tags > /dev/null 2>&1 80 | # shellcheck disable=SC2181 81 | [[ $? != 0 ]] && version+="+git.$(git rev-parse --short HEAD)" 82 | 83 | # BUG: Clobbers variables from config file 84 | [[ -z $ATFILE_FORCE_META_AUTHOR ]] && ATFILE_FORCE_META_AUTHOR="$author" 85 | [[ -z $ATFILE_FORCE_META_DID ]] && ATFILE_FORCE_META_DID="$did" 86 | [[ -z $ATFILE_FORCE_META_REPO ]] && ATFILE_FORCE_META_REPO="$repo" 87 | [[ -z $ATFILE_FORCE_META_YEAR ]] && ATFILE_FORCE_META_YEAR="$year" 88 | [[ -z $ATFILE_FORCE_VERSION ]] && ATFILE_FORCE_VERSION="$version" 89 | 90 | declare -a ATFILE_DEVEL_INCLUDES 91 | 92 | for f in "$ATFILE_DEVEL_DIR/src/commands/"*; do ATFILE_DEVEL_INCLUDES+=("$f"); done 93 | for f in "$ATFILE_DEVEL_DIR/src/lexi/"*; do ATFILE_DEVEL_INCLUDES+=("$f"); done 94 | for f in "$ATFILE_DEVEL_DIR/src/shared/"*; do ATFILE_DEVEL_INCLUDES+=("$f"); done 95 | ATFILE_DEVEL_INCLUDES+=("$ATFILE_DEVEL_DIR/src/entry.sh") 96 | 97 | for path in "${ATFILE_DEVEL_INCLUDES[@]}" 98 | do 99 | if [[ ! -f "$path" ]]; then 100 | atfile.devel.die "Unable to find source for '$path'" 101 | fi 102 | 103 | # shellcheck disable=SC1090 104 | source "$path" 105 | done 106 | -------------------------------------------------------------------------------- /src/commands/auth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC2120 4 | function atfile.auth() { 5 | override_username="$1" 6 | override_password="$2" 7 | 8 | [[ -n "$override_password" ]] && _password="$override_password" 9 | [[ -n "$override_username" ]] && _username="$override_username" 10 | 11 | if [[ -z "$_server" ]]; then 12 | skip_resolving=0 13 | 14 | # shellcheck disable=SC2154 15 | if [[ -z $override_username ]] && [[ $_is_sourced == 0 ]]; then 16 | # NOTE: Speeds things up a little if the user is overriding actor 17 | # Keep this in-sync with the main case in `../entry.sh`! 18 | if [[ $_command == "bsky" && -n "${_command_args[1]}" ]] ||\ 19 | [[ $_command == "cat" && -n "${_command_args[1]}" ]] ||\ 20 | [[ $_command == "fetch" && -n "${_command_args[1]}" ]] ||\ 21 | [[ $_command == "fetch-crypt" && -n "${_command_args[1]}" ]] ||\ 22 | [[ $_command == "info" && -n "${_command_args[1]}" ]] ||\ 23 | [[ $_command == "list" && "${_command_args[0]}" == *.* ]] ||\ 24 | [[ $_command == "list" && "${_command_args[0]}" == did:* ]] ||\ 25 | [[ $_command == "list" && -n "${_command_args[1]}" ]] ||\ 26 | [[ $_command == "url" && -n "${_command_args[1]}" ]]; then 27 | atfile.say.debug "Skipping identity resolving\n↳ Actor is overridden by command ('$_command')" 28 | skip_resolving=1 29 | fi 30 | 31 | # NOTE: Speeds things up a little if the command doesn't need actor resolving 32 | if [[ -z $_command ]] ||\ 33 | [[ $_command == "ai" ]] ||\ 34 | [[ $_command == "handle" ]] ||\ 35 | [[ $_command == "help" ]] ||\ 36 | [[ $_command == "now" ]] ||\ 37 | [[ $_command == "release" ]] ||\ 38 | [[ $_command == "resolve" ]] ||\ 39 | [[ $_command == "scrape" ]] ||\ 40 | [[ $_command == "something-broke" ]] ||\ 41 | [[ $_command == "stream" ]] ||\ 42 | [[ $_command == "update" ]] ||\ 43 | [[ $_command == "version" ]]; then 44 | atfile.say.debug "Skipping identity resolving\n↳ Not required for command '$_command'" 45 | skip_resolving=1 46 | fi 47 | fi 48 | 49 | if [[ $skip_resolving == 0 ]]; then 50 | [[ -z "$_username" || "$_username" == "" ]] && atfile.die "\$${_envvar_prefix}_USERNAME not set" 51 | [[ -z "$_password" || "$_password" == "" ]] && atfile.die "\$${_envvar_prefix}_PASSWORD not set" 52 | 53 | atfile.say.debug "Authenticating as '$_username'..." 54 | 55 | resolved_did="$(atfile.util.resolve_identity "$_username")" 56 | error="$(atfile.util.get_xrpc_error $? "$resolved_did")" 57 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to resolve '$_username'" "$resolved_did" 58 | 59 | _username="$(echo "$resolved_did" | cut -d "|" -f 1)" 60 | _server="$(echo "$resolved_did" | cut -d "|" -f 2)" 61 | 62 | atfile.say.debug "Resolved identity\n↳ DID: $_username\n↳ PDS: $_server" 63 | fi 64 | else 65 | atfile.say.debug "Skipping identity resolving\n↳ ${_envvar_prefix}_ENDPOINT_PDS is set ($_server)" 66 | [[ $_server != "http://"* ]] && [[ $_server != "https://"* ]] && _server="https://$_server" 67 | fi 68 | 69 | if [[ -n $_server ]]; then 70 | # shellcheck disable=SC2154 71 | if [[ $_disable_auth_check == 0 ]]; then 72 | atfile.say.debug "Checking authentication is valid..." 73 | 74 | session="$(com.atproto.server.getSession)" 75 | error="$(atfile.util.get_xrpc_error $? "$session")" 76 | 77 | if [[ -n "$error" ]]; then 78 | atfile.die.xrpc_error "Unable to authenticate" "$error" 79 | else 80 | _username="$(echo "$session" | jq -r ".did")" 81 | fi 82 | else 83 | atfile.say.debug "Skipping checking authentication validity\n↳ ${_envvar_prefix}_DISABLE_AUTH_CHECK is set ($_disable_auth_check)" 84 | if [[ "$_username" != "did:"* ]]; then 85 | atfile.die "Cannot skip authentication validation without a DID\n↳ \$${_envvar_prefix}_USERNAME currently set to '$_username' (need \"did::\")" 86 | fi 87 | fi 88 | fi 89 | } 90 | -------------------------------------------------------------------------------- /src/commands/record.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC2120 4 | function atfile.record() { 5 | function atfile.record.output_record() { 6 | # shellcheck disable=SC2154 7 | if [[ "$_output_json" == 1 ]]; then 8 | echo "{ \"uri\": \"$1\", \"cid\": \"$2\", \"record\": $3 }" | jq 9 | else 10 | echo "⚡ $1" 11 | echo "📦 $2" 12 | echo "---" 13 | echo "$3" | jq 14 | fi 15 | } 16 | 17 | sub_command="$1" 18 | at_uri="$2" 19 | record="$3" 20 | 21 | unset record_json 22 | unset output_json 23 | unset output_return 24 | unset at_actor 25 | unset at_collection 26 | unset at_rkey 27 | 28 | if [[ "$sub_command" == "create" && "$at_uri" != "at://"* ]]; then 29 | # shellcheck disable=SC2154 30 | at_uri="at://$_username/$at_uri" 31 | fi 32 | 33 | if [[ "$sub_command" != "delete" ]] &&\ 34 | [[ "$sub_command" != "get" ]]; then 35 | if [[ -z "$record" ]]; then 36 | atfile.die " not set" 37 | else 38 | record_json="$(echo "$record" | jq)" 39 | # shellcheck disable=SC2181 40 | [[ $? != 0 ]] && atfile.die "Invalid JSON" 41 | fi 42 | fi 43 | 44 | [[ "$at_uri" != "at://"* ]] && atfile.die \ 45 | "Invalid AT URI\n↳ Must be 'at://[/[/]]'" 46 | 47 | at_actor="$(atfile.util.parse_at_uri "$at_uri" "actor")" 48 | at_collection="$(atfile.util.parse_at_uri "$at_uri" "collection")" 49 | at_rkey="$(atfile.util.parse_at_uri "$at_uri" "rkey")" 50 | 51 | case "$sub_command" in 52 | "create") 53 | if [[ -z "$at_rkey" ]]; then 54 | output_json="$(com.atproto.repo.createRecord "$at_actor" "$at_collection" "$record_json")" 55 | output_return="$?" 56 | else 57 | output_json="$(com.atproto.repo.putRecord "$at_actor" "$at_collection" "$at_rkey" "$record_json")" 58 | output_return="$?" 59 | fi 60 | ;; 61 | "delete") 62 | output_json="$(com.atproto.repo.deleteRecord "$at_actor" "$at_collection" "$at_rkey")" 63 | output_return="$?" 64 | ;; 65 | "get") 66 | output_json="$(com.atproto.repo.getRecord "$at_actor" "$at_collection" "$at_rkey")" 67 | output_return="$?" 68 | ;; 69 | "recreate") 70 | output_json="$(com.atproto.repo.deleteRecord "$at_actor" "$at_collection" "$at_rkey")" 71 | output_return="$?" 72 | output_json="$(com.atproto.repo.putRecord "$at_actor" "$at_collection" "$at_rkey" "$record_json")" 73 | output_return="$?" 74 | ;; 75 | "update") 76 | output_json="$(com.atproto.repo.putRecord "$at_actor" "$at_collection" "$at_rkey" "$record_json")" 77 | output_return="$?" 78 | ;; 79 | esac 80 | 81 | error="$(atfile.util.get_xrpc_error "$output_return" "$output_json")" 82 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to $sub_command '$at_uri'" "$output_json" 83 | 84 | case "$sub_command" in 85 | "create"|"recreate"|"update") 86 | atfile.record.output_record \ 87 | "$(echo "$output_json" | jq -r .uri)" \ 88 | "$(echo "$output_json" | jq -r .commit.cid)" \ 89 | "$record_json" 90 | ;; 91 | "delete") 92 | atfile.record.output_record \ 93 | "$at_uri" \ 94 | "$(echo "$output_json" | jq -r .commit.cid)" \ 95 | "null" 96 | ;; 97 | *) 98 | atfile.record.output_record \ 99 | "$(echo "$output_json" | jq -r .uri)" \ 100 | "$(echo "$output_json" | jq -r .cid)" \ 101 | "$(echo "$output_json" | jq -r .value)" 102 | ;; 103 | esac 104 | } 105 | 106 | function atfile.record_list() { 107 | at_uri="$1" 108 | 109 | unset at_actor 110 | unset at_collection 111 | unset at_rkey 112 | 113 | [[ "$at_uri" != "at://"* ]] && atfile.die \ 114 | "Invalid AT URI\n↳ Must be 'at://[/]'" 115 | 116 | at_actor="$(atfile.util.parse_at_uri "$at_uri" "actor")" 117 | at_collection="$(atfile.util.parse_at_uri "$at_uri" "collection")" 118 | at_rkey="$(atfile.util.parse_at_uri "$at_uri" "rkey")" 119 | 120 | if [[ -n "$at_rkey" ]]; then 121 | atfile.record get "$at_uri" 122 | return 123 | fi 124 | 125 | atfile.die "Not yet implemented!" 126 | } 127 | -------------------------------------------------------------------------------- /src/commands/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.install() { 4 | override_path="$1" 5 | override_version="$2" 6 | override_did="$3" 7 | 8 | # shellcheck disable=SC2154 9 | [[ $_output_json == 1 ]] && atfile.die "Command not available as JSON" 10 | 11 | uid="$(id -u)" 12 | # shellcheck disable=SC2154 13 | conf_dir="${_path_envvar%/*}" 14 | install_file="atfile" 15 | unset found_version 16 | unset install_dir 17 | unset source_did 18 | 19 | atfile.util.check_prog "curl" 20 | atfile.util.check_prog "jq" 21 | 22 | # shellcheck disable=SC2154 23 | if [[ $_os_supported == 0 ]]; then 24 | atfile.die "Unsupported OS (${_os//unknown-/})" 25 | fi 26 | 27 | case $_os in 28 | "haiku") 29 | install_dir="/boot/system/non-packaged/bin" 30 | ;; 31 | *) 32 | if [[ $uid == 0 ]]; then 33 | install_dir="/usr/local/bin" 34 | else 35 | # shellcheck disable=SC2154 36 | install_dir="$_path_home/.local/bin" 37 | fi 38 | ;; 39 | esac 40 | 41 | if [[ $# -gt 0 ]]; then 42 | atfile.say.debug "Overriden variables\n↳ Path: $override_path\n↳ Version: $override_version\n↳ DID: $override_did" 43 | fi 44 | 45 | atfile.say.debug "Setting up..." 46 | 47 | [[ -n "$override_path" ]] && install_dir="$override_path" 48 | mkdir -p "$conf_dir" 49 | # shellcheck disable=SC2154 50 | touch "$conf_dir/$_file_envvar" 51 | 52 | if [[ -f "$install_dir/$install_file" ]]; then 53 | atfile.die "Already installed ($install_dir/$install_file)" 54 | fi 55 | 56 | atfile.say.debug "Resolving latest version..." 57 | 58 | # shellcheck disable=SC2154 59 | if [[ -z "$override_did" ]]; then 60 | if [[ $_meta_did == "{:"*":}" ]]; then 61 | # shellcheck disable=SC2154 62 | source_did="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_META_DID")" 63 | else 64 | source_did="$_meta_did" 65 | fi 66 | else 67 | source_did="$override_did" 68 | fi 69 | 70 | atfile.util.override_actor "$source_did" 71 | # BUG: $_fmt_blob_url_default is unpopulated 72 | _fmt_blob_url="[server]/xrpc/com.atproto.sync.getBlob?did=[did]&cid=[cid]" 73 | 74 | if [[ -z "$override_version" ]]; then 75 | latest_version_record="$(com.atproto.repo.getRecord "$source_did" "self.atfile.latest" "self")" 76 | error="$(atfile.util.get_xrpc_error $? "$latest_version_record")" 77 | [[ -n "$error" ]] && atfile.die "Unable to fetch latest version" 78 | 79 | found_version="$(echo "$latest_version_record" | jq -r '.value.version')" 80 | else 81 | found_version="$override_version" 82 | fi 83 | 84 | parsed_found_version="$(atfile.util.parse_version "$found_version")" 85 | 86 | found_version_key="atfile-$parsed_found_version" 87 | found_version_record="$(com.atproto.repo.getRecord "$source_did" "blue.zio.atfile.upload" "$found_version_key")" 88 | error="$(atfile.util.get_xrpc_error $? "$found_version_record")" 89 | [[ -n "$error" ]] && atfile.die "Unable to fetch record for '$found_version'" 90 | 91 | found_version_blob="$(echo "$found_version_record" | jq -r ".value.blob.ref.\"\$link\"")" 92 | # shellcheck disable=SC2154 93 | blob_url="$(atfile.util.build_blob_uri "$source_did" "$found_version_blob" "$_server")" 94 | 95 | atfile.say.debug "Found latest version\n↳ Version: $found_version ($parsed_found_version)\n↳ Source: $source_did\n↳ Blob: $found_version_blob" 96 | 97 | atfile.say.debug "Download '$blob_url'..." 98 | 99 | curl -s -o "${install_dir}/$install_file" "$blob_url" 100 | [[ $? != 0 ]] && atfile.die "Unable to download" 101 | 102 | atfile.say.debug "Installing...\n↳ OS: $_os\n↳ Install: $install_dir/$install_file\n↳ Config: $conf_dir/$_file_envvar" 103 | 104 | chmod +x "${install_dir}/$install_file" 105 | [[ $? != 0 ]] && atfile.die "Unable to set as executable" 106 | 107 | if [[ ! -f "$conf_dir/$_file_envvar" ]] || [[ ! -s "$conf_dir/$_file_envvar" ]]; then 108 | atfile.say.debug "Creating config file..." 109 | 110 | echo -e "ATFILE_USERNAME=\nATFILE_PASSWORD=" > "$conf_dir/$_file_envvar" 111 | [[ $? != 0 ]] && die "Unable to create config file ($conf_dir/$_file_envvar)" 112 | fi 113 | 114 | atfile.say "😎 Installed ATFile" 115 | atfile.say " ↳ Version: $found_version" 116 | atfile.say " ↳ Paths" 117 | atfile.say " ↳ Install: $install_dir/$install_file" 118 | atfile.say " ↳ Config: $conf_dir/$_file_envvar" 119 | atfile.say " ↳ Source: atfile://$source_did/$found_version_key" 120 | atfile.say " ---" 121 | atfile.say " Before running, set your credentials in the config file!" 122 | atfile.say " Run '$install_file help' to get started" 123 | # ------------------------------------------------------------------------------ 124 | 125 | atfile.util.override_actor_reset 126 | } 127 | -------------------------------------------------------------------------------- /src/commands/bsky_profile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.bsky_profile() { 4 | actor="$1" 5 | 6 | # shellcheck disable=SC2154 7 | [[ $_output_json == 1 ]] && atfile.die "Command not available as JSON" 8 | 9 | function atfile.bsky_profile.get_pretty_date() { 10 | date="$1" 11 | 12 | if [[ $date == "null" ]]; then 13 | echo "(Unknown)" 14 | else 15 | # shellcheck disable=SC2317 16 | atfile.util.get_date "$1" "%Y-%m-%d %H:%M:%S" 17 | fi 18 | } 19 | 20 | if [[ -z "$actor" ]]; then 21 | # shellcheck disable=SC2154 22 | actor="$_username" 23 | else 24 | resolved_did="$(atfile.util.resolve_identity "$actor")" 25 | error="$(atfile.util.get_xrpc_error $? "$resolved_did")" 26 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to resolve '$actor'" "$resolved_did" 27 | 28 | actor="$(echo "$resolved_did" | cut -d "|" -f 1)" 29 | fi 30 | 31 | bsky_profile="$(app.bsky.actor.getProfile "$actor")" 32 | error="$(atfile.util.get_xrpc_error $? "$bsky_profile")" 33 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to get Bluesky profile for '$actor'" "$bsky_profile" 34 | 35 | bio="$(echo "$bsky_profile" | jq '.description')" 36 | bio="${bio%\"}"; bio="${bio#\"}" 37 | count_feeds="$(echo "$bsky_profile" | jq -r '.associated.feedgens')" 38 | count_followers="$(echo "$bsky_profile" | jq -r '.followersCount')" 39 | count_following="$(echo "$bsky_profile" | jq -r '.followsCount')" 40 | count_likes=0 41 | count_lists="$(echo "$bsky_profile" | jq -r '.associated.lists')" 42 | count_packs="$(echo "$bsky_profile" | jq -r '.associated.starterPacks')" 43 | count_posts="$(echo "$bsky_profile" | jq -r '.postsCount')" 44 | date_created="$(echo "$bsky_profile" | jq -r '.createdAt')" 45 | date_created="$(atfile.bsky_profile.get_pretty_date "$date_created")" 46 | date_indexed="$(echo "$bsky_profile" | jq -r '.indexedAt')" 47 | date_indexed="$(atfile.bsky_profile.get_pretty_date "$date_indexed")" 48 | did="$(echo "$bsky_profile" | jq -r '.did')" 49 | handle="$(echo "$bsky_profile" | jq -r '.handle')" 50 | name="$(echo "$bsky_profile" | jq -r '.displayName')" 51 | type="🔵 User" 52 | 53 | if [[ $(atfile.util.is_null_or_empty "$bio") == 1 ]]; then 54 | bio="(No Bio)" 55 | else 56 | bio="$(echo -e "$bio" | fold -sw 78)" 57 | unset bio_formatted 58 | 59 | while IFS= read -r line; do 60 | bio_formatted+=" $line\n" 61 | done <<< "$bio" 62 | fi 63 | 64 | if [[ "$(echo "$bsky_profile" | jq -r '.associated.labeler')" == "true" ]]; then 65 | labeler_services="$(app.bsky.labeler.getServices "$did")" 66 | 67 | count_likes="$(echo "$labeler_services" | jq -r '.views[] | select(."$type" == "app.bsky.labeler.defs#labelerView") | .likeCount')" 68 | type="🟦 Labeler" 69 | fi 70 | 71 | [[ $(atfile.util.is_null_or_empty "$count_feeds") == 1 ]] && count_feeds="0" || count_feeds="$(atfile.util.fmt_int "$count_feeds")" 72 | [[ $(atfile.util.is_null_or_empty "$count_followers") == 1 ]] && count_followers="0" || count_followers="$(atfile.util.fmt_int "$count_followers")" 73 | [[ $(atfile.util.is_null_or_empty "$count_following") == 1 ]] && count_following="0" || count_following="$(atfile.util.fmt_int "$count_following")" 74 | [[ $(atfile.util.is_null_or_empty "$count_likes") == 1 ]] && count_likes="0" || count_likes="$(atfile.util.fmt_int "$count_likes")" 75 | [[ $(atfile.util.is_null_or_empty "$count_lists") == 1 ]] && count_lists="0" || count_lists="$(atfile.util.fmt_int "$count_lists")" 76 | [[ $(atfile.util.is_null_or_empty "$count_packs") == 1 ]] && count_packs="0" || count_packs="$(atfile.util.fmt_int "$count_packs")" 77 | [[ $(atfile.util.is_null_or_empty "$count_posts") == 1 ]] && count_posts="0" || count_posts="$(atfile.util.fmt_int "$count_posts")" 78 | [[ $(atfile.util.is_null_or_empty "$handle") == 1 ]] && handle="handle.invalid" 79 | [[ $(atfile.util.is_null_or_empty "$name") == 1 ]] && name="$handle" 80 | 81 | name_length=${#name} 82 | 83 | # Do not modify the spacing here! 84 | bsky_profile_output=" 85 | \e[1;37m$name\e[0m 86 | \e[37m$(atfile.util.repeat_char "-" "$name_length")\e[0m 87 | $bio_formatted \e[37m$(atfile.util.repeat_char "-" 3)\e[0m 88 | 🔌 @$handle ∙ #️⃣ $did 89 | ⬇️ $count_followers $(atfile.util.get_int_suffix "$count_followers" "\e[37mFollower\e[0m" "\e[37mFollowers\e[0m") ∙ ⬆️ $count_following \e[37mFollowing\e[0m ∙ ⭐️ $count_likes $(atfile.util.get_int_suffix "$count_likes" "\e[37mLike\e[0m" "\e[37mLikes\e[0m") 90 | 📃 $count_posts $(atfile.util.get_int_suffix "$count_followers" "\e[37mPost\e[0m" "\e[37mPosts\e[0m") ∙ ⚙️ $count_feeds $(atfile.util.get_int_suffix "$count_feeds" "\e[37mFeed\e[0m" "\e[37mFeeds\e[0m") ∙ 📋 $count_lists $(atfile.util.get_int_suffix "$count_lists" "\e[37mList\e[0m" "\e[37mLists\e[0m") ∙ 👥 $count_packs $(atfile.util.get_int_suffix "$count_packs" "\e[37mPack\e[0m" "\e[37mPacks\e[0m") 91 | $type ∙ ✨ $date_created ∙ 🕷️ $date_indexed 92 | \e[37m$(atfile.util.repeat_char "-" 3)\e[0m 93 | 🦋 $_endpoint_social_app/profile/$actor\n" 94 | 95 | if [[ "$date_indexed" == "0001-01-01 00:00:00" ]]; then 96 | atfile.die "No Bluesky profile for '$actor'" 97 | else 98 | atfile.say "$bsky_profile_output" 99 | fi 100 | } 101 | -------------------------------------------------------------------------------- /src/commands/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: Validate checksum 4 | function atfile.update() { 5 | cmd="$1" 6 | unset error 7 | is_git=0 8 | 9 | if [ -x "$(command -v git)" ] && [[ -d "$_prog_dir/.git" ]] && [[ "$(atfile.util.get_realpath "$(pwd)")" == "$_prog_dir" ]]; then 10 | is_git=1 11 | fi 12 | 13 | if [[ "$cmd" == "check-only" ]]; then 14 | # shellcheck disable=SC2154 15 | [[ $_disable_update_checking == 1 ]] && return 16 | # shellcheck disable=SC2154 17 | [[ $_disable_update_command == 1 ]] && return 18 | # shellcheck disable=SC2154 19 | [[ $is_git == 1 && $_enable_update_git_clobber == 0 ]] && return 20 | # shellcheck disable=SC2154 21 | [[ $_output_json == 1 ]] && return 22 | 23 | last_checked="$(atfile.cache.get "update-check")" 24 | current_checked="$(atfile.util.get_date "" "%s")" 25 | check_sleep=3600 26 | next_check=$(( last_checked + check_sleep )) 27 | 28 | atfile.say.debug "Checking for last update check...\n↳ Last: $last_checked\n↳ Cur.: $current_checked\n↳ Next: $next_check" 29 | 30 | if [[ $(( next_check < current_checked )) == 0 ]]; then 31 | return 32 | else 33 | last_checked="$(atfile.cache.set "update-check" "$current_checked")" 34 | fi 35 | fi 36 | 37 | [[ $_output_json == 1 ]] && atfile.die "Command not available as JSON" 38 | 39 | # shellcheck disable=SC2154 40 | update_did="$_devel_dist_username" 41 | 42 | atfile.util.override_actor "$update_did" 43 | 44 | atfile.say.debug "Getting latest release..." 45 | latest_release_record="$(com.atproto.repo.getRecord "$update_did" "self.atfile.latest" "self")" 46 | error="$(atfile.util.get_xrpc_error $? "$latest_release_record")" 47 | 48 | [[ -n "$error" ]] && atfile.die "Unable to get latest version" "$error" 49 | 50 | latest_version="$(echo "$latest_release_record" | jq -r '.value.version')" 51 | latest_version_commit="$(echo "$latest_release_record" | jq -r '.value.commit')" 52 | latest_version_date="$(echo "$latest_release_record" | jq -r '.value.releasedAt')" 53 | parsed_latest_version="$(atfile.util.parse_version "$latest_version")" 54 | parsed_running_version="$(atfile.util.parse_version "$_version")" 55 | latest_version_record_id="atfile-$parsed_latest_version" 56 | update_available=0 57 | 58 | atfile.say.debug "Checking version...\n↳ Latest: $latest_version ($parsed_latest_version)\n ↳ Date: $latest_version_date\n ↳ Commit: $latest_version_commit\n↳ Running: $_version ($parsed_running_version)" 59 | 60 | if [[ $(( parsed_latest_version > parsed_running_version )) == 1 ]]; then 61 | update_available=1 62 | fi 63 | 64 | case "$cmd" in 65 | "check-only") 66 | if [[ $update_available == 0 ]]; then 67 | atfile.say.debug "No updates found" 68 | return 69 | fi 70 | 71 | echo "---" 72 | if [[ $_os == "haiku" ]]; then 73 | # BUG: Haiku Terminal has issues with emojis 74 | # shellcheck disable=SC2154 75 | atfile.say "Update available ($latest_version)\n↳ Run \`$_prog update\` to update" 76 | else 77 | atfile.say "😎 Update available ($latest_version)\n ↳ Run \`$_prog update\` to update" 78 | fi 79 | ;; 80 | "install") 81 | [[ $is_git == 1 && $_enable_update_git_clobber == 0 ]] &&\ 82 | atfile.die "Cannot update in Git repository" 83 | [[ $_disable_update_command == 1 ]] &&\ 84 | atfile.die "Cannot update system-managed version: update from your package manager" 85 | 86 | if [[ $update_available == 0 ]]; then 87 | atfile.say "No updates found" 88 | return 89 | fi 90 | 91 | # shellcheck disable=SC2154 92 | temp_updated_path="$_prog_dir/${_prog}-${latest_version}.tmp" 93 | 94 | atfile.say.debug "Touching temporary path ($temp_updated_path)..." 95 | touch "$temp_updated_path" 96 | # shellcheck disable=SC2181 97 | [[ $? != 0 ]] && atfile.die "Unable to create temporary file (do you have permission?)" 98 | 99 | atfile.say.debug "Getting blob URL for $latest_version ($latest_version_record_id)..." 100 | blob_url="$(atfile.invoke.get_url "$latest_version_record_id")" 101 | # shellcheck disable=SC2181 102 | [[ $? != 0 ]] && atfile.die "Unable to get blob URL" 103 | 104 | atfile.say.debug "Downloading latest release..." 105 | curl -H "User-Agent: $(atfile.util.get_uas)" -s -o "$temp_updated_path" "$blob_url" 106 | # shellcheck disable=SC2181 107 | if [[ $? == 0 ]]; then 108 | # shellcheck disable=SC2154 109 | mv "$temp_updated_path" "$_prog_path" 110 | # shellcheck disable=SC2181 111 | if [[ $? != 0 ]]; then 112 | atfile.die "Unable to update (do you have permission?)" 113 | else 114 | chmod +x "$_prog_path" 115 | 116 | if [[ $_os == "haiku" ]]; then 117 | atfile.say "Updated to $latest_version!" # BUG: Haiku Terminal has issues with emojis 118 | else 119 | atfile.say "😎 Updated to $latest_version!" 120 | fi 121 | 122 | last_checked="$(atfile.cache.set "update-check" "$current_checked")" 123 | 124 | return 125 | fi 126 | else 127 | atfile.die "Unable to download latest version" 128 | fi 129 | ;; 130 | esac 131 | } 132 | -------------------------------------------------------------------------------- /src/commands/something_broke.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.something_broke() { 4 | prog_not_installed_placeholder="(Not Installed)" 5 | 6 | function atfile.something_broke.print_envvar() { 7 | # shellcheck disable=SC2154 8 | variable_name="${_envvar_prefix}_$1" 9 | variable_default="$2" 10 | 11 | unset output 12 | 13 | output="$variable_name: $(atfile.util.get_envvar "$variable_name" "$variable_default")" 14 | [[ -n "$variable_default" ]] && output+=" [$variable_default]" 15 | 16 | echo -e "↳ $output" 17 | } 18 | 19 | function atfile.something_broke.print_prog_version() { 20 | prog="$1" 21 | version_arg="$2" 22 | 23 | [[ -z "$version_arg" ]] && version_arg="--version" 24 | 25 | if [ -x "$(command -v "$prog")" ]; then 26 | eval "$prog $version_arg 2>&1" 27 | else 28 | echo "$prog_not_installed_placeholder" 29 | fi 30 | } 31 | 32 | # shellcheck disable=SC2154 33 | if [[ $_output_json == 1 ]]; then 34 | atfile.die "Command not available as JSON" 35 | fi 36 | 37 | unset md5sum_version 38 | finger_record="$(atfile.util.get_finger_record 1)" 39 | mediainfo_version="$(atfile.something_broke.print_prog_version "mediainfo")" 40 | 41 | # shellcheck disable=SC2154 42 | if [[ $_os == "linux-musl" ]]; then 43 | md5sum_version="$(atfile.something_broke.print_prog_version "md5sum" "--help")" 44 | else 45 | md5sum_version="$(atfile.something_broke.print_prog_version "md5sum")" 46 | fi 47 | 48 | if [[ "$md5sum_version" != "$prog_not_installed_placeholder" ]]; then 49 | md5sum_version="$(echo "$md5sum_version" | head -n 1)" 50 | if [[ "$md5sum_version" == *GNU* ]]; then 51 | md5sum_version="$(echo "$md5sum_version" | cut -d " " -f 4) (GNU)" 52 | elif [[ "$md5sum_version" == *BusyBox* ]]; then 53 | md5sum_version="$(echo "$md5sum_version" | cut -d " " -f 2 | cut -d "v" -f 2) (BusyBox)" 54 | else 55 | md5sum_version="(?)" 56 | fi 57 | fi 58 | 59 | if [[ "$mediainfo_version" != "$prog_not_installed_placeholder" ]]; then 60 | mediainfo_version="$(echo "$mediainfo_version" | grep "MediaInfoLib" | cut -d "v" -f 2)" 61 | fi 62 | 63 | debug_output="ATFile 64 | ↳ Version: $_version 65 | ↳ UAS: $(atfile.util.get_uas) 66 | ↳ Path: $_prog_path 67 | Variables 68 | $(atfile.something_broke.print_envvar "DEBUG") 69 | $(atfile.something_broke.print_envvar "DEVEL") 70 | $(atfile.something_broke.print_envvar "DEVEL_DIR") 71 | ↳ ${_envvar_prefix}_DEVEL_DIST_PASSWORD: $([[ -n $(atfile.util.get_envvar "${_envvar_prefix}_DEVEL_DIST_PASSWORD") ]] && echo "(Set)") 72 | $(atfile.something_broke.print_envvar "DEVEL_DIST_USERNAME" "$_devel_dist_username_default") 73 | $(atfile.something_broke.print_envvar "DEVEL_ENABLE_PIPING" "0") 74 | $(atfile.something_broke.print_envvar "DEVEL_ENABLE_PUBLISH" "$_devel_enable_publish_default") 75 | $(atfile.something_broke.print_envvar "DEVEL_ENTRY") 76 | ↳ ${_envvar_prefix}_DEVEL_INCLUDES: 77 | $(for s in "${ATFILE_DEVEL_INCLUDES[@]}"; do echo " ↳ $s"; done) 78 | $(atfile.something_broke.print_envvar "DEVEL_SOURCE") 79 | $(atfile.something_broke.print_envvar "DISABLE_AUTH_CHECK" "$_disable_auth_check_default") 80 | $(atfile.something_broke.print_envvar "DISABLE_NI_EXIFTOOL" "$_disable_ni_exiftool_default") 81 | $(atfile.something_broke.print_envvar "DISABLE_NI_MD5SUM" "$_disable_ni_md5sum_default") 82 | $(atfile.something_broke.print_envvar "DISABLE_NI_MEDIAINFO" "$_disable_ni_mediainfo_default") 83 | $(atfile.something_broke.print_envvar "DISABLE_SETUP_DIR_CREATION" "$_disable_setup_dir_creation_default") 84 | $(atfile.something_broke.print_envvar "DISABLE_UNSUPPORTED_OS_WARN" "$_disable_unsupported_os_warn") 85 | $(atfile.something_broke.print_envvar "DISABLE_UPDATE_CHECKING" "$_disable_update_checking_default") 86 | $(atfile.something_broke.print_envvar "DISABLE_UPDATE_COMMAND" "$_disable_update_command_default") 87 | $(atfile.something_broke.print_envvar "ENABLE_FINGERPRINT" "$_enable_fingerprint_default") 88 | $(atfile.something_broke.print_envvar "ENABLE_UPDATE_GIT_CLOBBER" "$_enable_update_git_clobber") 89 | $(atfile.something_broke.print_envvar "ENDPOINT_APPVIEW" "$_endpoint_appview_default") 90 | $(atfile.something_broke.print_envvar "ENDPOINT_JETSTREAM" "$_endpoint_jetstream_default") 91 | $(atfile.something_broke.print_envvar "ENDPOINT_PDS") 92 | $(atfile.something_broke.print_envvar "ENDPOINT_PLC_DIRECTORY" "$_endpoint_plc_directory_default") 93 | $(atfile.something_broke.print_envvar "ENDPOINT_SOCIAL_APP" "$_endpoint_social_app_default") 94 | $(atfile.something_broke.print_envvar "FMT_BLOB_URL" "$_fmt_blob_url_default") 95 | $(atfile.something_broke.print_envvar "FMT_OUT_FILE" "$_fmt_out_file_default") 96 | $(atfile.something_broke.print_envvar "FORCE_META_AUTHOR") 97 | $(atfile.something_broke.print_envvar "FORCE_META_DID") 98 | $(atfile.something_broke.print_envvar "FORCE_META_REPO") 99 | $(atfile.something_broke.print_envvar "FORCE_META_YEAR") 100 | $(atfile.something_broke.print_envvar "FORCE_NOW") 101 | $(atfile.something_broke.print_envvar "FORCE_OS") 102 | $(atfile.something_broke.print_envvar "FORCE_VERSION") 103 | $(atfile.something_broke.print_envvar "MAX_LIST" "$_max_list_default") 104 | $(atfile.something_broke.print_envvar "OUTPUT_JSON" "$_output_json_default") 105 | $(atfile.something_broke.print_envvar "PATH_CONF" "$_path_envvar") 106 | ↳ ${_envvar_prefix}_PASSWORD: $([[ -n $(atfile.util.get_envvar "${_envvar_prefix}_PASSWORD") ]] && echo "(Set)") 107 | $(atfile.something_broke.print_envvar "USERNAME") 108 | Paths 109 | ↳ Blobs: $_path_blobs_tmp 110 | ↳ Cache: $_path_cache 111 | ↳ Config: $_path_envvar 112 | Environment 113 | ↳ OS: $_os ($(echo "$finger_record" | jq -r ".os")) 114 | ↳ Shell: $SHELL 115 | ↳ Path: $PATH 116 | Deps 117 | ↳ Bash: $BASH_VERSION 118 | ↳ curl: $(atfile.something_broke.print_prog_version "curl" "--version" | head -n 1 | cut -d " " -f 2) 119 | ↳ ExifTool: $(atfile.something_broke.print_prog_version "exiftool" "-ver") 120 | ↳ jq: $(atfile.something_broke.print_prog_version "jq" | sed -e "s|jq-||g") 121 | ↳ md5sum: $md5sum_version 122 | ↳ MediaInfo: $mediainfo_version 123 | Misc. 124 | ↳ Checksum: $([[ "$md5sum_version" != "$prog_not_installed_placeholder" ]] && md5sum "$_prog_path" || echo "(?)") 125 | ↳ Dimensions: $(atfile.util.get_term_cols) Cols / $(atfile.util.get_term_rows) Rows 126 | ↳ Now: $_now 127 | ↳ Sudo: $SUDO_USER" 128 | 129 | atfile.say "$debug_output" 130 | } 131 | -------------------------------------------------------------------------------- /src/commands/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # atfile-devel=ignore-build 3 | 4 | function atfile.release() { 5 | # shellcheck disable=SC2154 6 | [[ $_os != "linux" ]] && atfile.die "Only available on Linux (GNU)\n↳ Detected OS: $_os" 7 | 8 | function atfile.release.get_devel_value() { 9 | local file="$1" 10 | local value="$2" 11 | local found_line 12 | 13 | found_line="$(grep '^# atfile-devel=' "$file" | head -n1)" 14 | if [[ -n "$found_line" ]]; then 15 | local devel_values="${found_line#*=}" 16 | IFS=',' read -ra arr <<< "$devel_values" 17 | for v in "${arr[@]}"; do 18 | if [[ "$v" == "$value" ]]; then 19 | echo 1 20 | fi 21 | done 22 | fi 23 | } 24 | 25 | function atfile.release.replace_template_var() { 26 | string="$1" 27 | key="$2" 28 | value="$3" 29 | 30 | sed -s "s|{:$key:}|$value|g" <<< "$string" 31 | } 32 | 33 | atfile.util.check_prog "git" 34 | atfile.util.check_prog "md5sum" 35 | atfile.util.check_prog "shellcheck" 36 | 37 | id="$(atfile.util.get_random 13)" 38 | commit_author="$(git config user.name) <$(git config user.email)>" 39 | commit_hash="$(git rev-parse HEAD)" 40 | commit_date="$(git show --no-patch --format=%ci "$commit_hash")" 41 | # shellcheck disable=SC2154 42 | dist_file="$(echo "$_prog" | cut -d "." -f 1)-${_version}.sh" 43 | # shellcheck disable=SC2154 44 | dist_dir="$_prog_dir/bin" 45 | dist_path="$dist_dir/$dist_file" 46 | dist_path_relative="$(realpath --relative-to="$(pwd)" "$dist_path")" 47 | parsed_version="$(atfile.util.parse_version "$_version")" 48 | version_record_id="atfile-$parsed_version" 49 | 50 | test_error_count=0 51 | test_info_count=0 52 | test_style_count=0 53 | test_warning_count=0 54 | test_ignore_count=0 55 | 56 | atfile.say "⚒️ Building..." 57 | 58 | echo "↳ Creating '$dist_file'..." 59 | mkdir -p "$dist_dir" 60 | echo "#!/usr/bin/env bash" > "$dist_path" 61 | 62 | echo "↳ Generating header..." 63 | echo -e "\n# ATFile <${ATFILE_FORCE_META_REPO}> 64 | # --- 65 | # Version: $_version 66 | # Commit: $commit_hash 67 | # Author: $commit_author 68 | # Build: $id ($(hostname):$(atfile.util.get_os)) 69 | # --- 70 | # Psst! You can \`source atfile\` in your own Bash scripts! 71 | " >> "$dist_path" 72 | 73 | for s in "${ATFILE_DEVEL_INCLUDES[@]}" 74 | do 75 | if [[ -f "$s" ]]; then 76 | if [[ $(atfile.release.get_devel_value "$s" "ignore-build" == 1 ) ]]; then 77 | echo "↳ Ignoring: $s" 78 | else 79 | echo "↳ Compiling: $s" 80 | 81 | while IFS="" read -r line 82 | do 83 | include_line=1 84 | 85 | if [[ $line == "#"* ]] ||\ 86 | [[ $line == *" #"* ]] ||\ 87 | [[ $line == " " ]] ||\ 88 | [[ $line == "" ]]; then 89 | include_line=0 90 | fi 91 | 92 | if [[ $line == *"# shellcheck disable"* ]]; then 93 | if [[ $line == *"=SC2154"* ]]; then 94 | include_line=0 95 | else 96 | include_line=1 97 | (( test_ignore_count++ )) 98 | fi 99 | fi 100 | 101 | if [[ $include_line == 1 ]]; then 102 | if [[ $line == *"{:"* && $line == *":}"* ]]; then 103 | # NOTE: Not using atfile.util.get_envvar() here, as confusion can arise from config file 104 | line="$(atfile.release.replace_template_var "$line" "meta_author" "$ATFILE_FORCE_META_AUTHOR")" 105 | line="$(atfile.release.replace_template_var "$line" "meta_did" "$ATFILE_FORCE_META_DID")" 106 | line="$(atfile.release.replace_template_var "$line" "meta_repo" "$ATFILE_FORCE_META_REPO")" 107 | line="$(atfile.release.replace_template_var "$line" "meta_year" "$ATFILE_FORCE_META_YEAR")" 108 | line="$(atfile.release.replace_template_var "$line" "version" "$ATFILE_FORCE_VERSION")" 109 | fi 110 | 111 | echo "$line" >> "$dist_path" 112 | fi 113 | done < "$s" 114 | fi 115 | fi 116 | done 117 | 118 | echo -e "\n# \"Four million lines of BASIC\"\n# - Kif Kroker (3003)" >> "$dist_path" 119 | 120 | checksum="$(atfile.util.get_md5 "$dist_path" | cut -d "|" -f 1)" 121 | 122 | echo "🧪 Testing..." 123 | shellcheck_output="$(shellcheck --format=json "$dist_path" 2>&1)" 124 | 125 | while read -r item; do 126 | code="$(echo "$item" | jq -r '.code')" 127 | col="$(echo "$item" | jq -r '.column')" 128 | line="$(echo "$item" | jq -r '.line')" 129 | level="$(echo "$item" | jq -r '.level')" 130 | message="$(echo "$item" | jq -r '.message')" 131 | 132 | case "$level" in 133 | "error") level="🛑 Error"; (( test_error_count++ )) ;; 134 | "info") level="ℹ️ Info"; (( test_info_count++ )) ;; 135 | "style") level="🎨 Style"; (( test_style_count++ )) ;; 136 | "warning") level="⚠️ Warning"; (( test_warning_count++ )) ;; 137 | esac 138 | 139 | echo "↳ $level ($line:$col): [SC$code] $message" 140 | done <<< "$(echo "$shellcheck_output" | jq -c '.[]')" 141 | 142 | test_total_count=$(( test_error_count + test_info_count + test_style_count + test_warning_count )) 143 | 144 | echo -e "---\n✅ Built: $_version 145 | ↳ Path: ./$dist_path_relative 146 | ↳ Check: $checksum 147 | ↳ Size: $(atfile.util.get_file_size_pretty "$(stat -c %s "$dist_path")") 148 | ↳ Lines: $(atfile.util.fmt_int "$(wc -l < "$dist_path")") 149 | ↳ Issues: $(atfile.util.fmt_int "$test_total_count") 150 | ↳ Error: $(atfile.util.fmt_int "$test_error_count") 151 | ↳ Warning: $(atfile.util.fmt_int "$test_warning_count") 152 | ↳ Info: $(atfile.util.fmt_int "$test_info_count") 153 | ↳ Style: $(atfile.util.fmt_int "$test_style_count") 154 | ↳ Ignored: $(atfile.util.fmt_int "$test_ignore_count") 155 | ↳ ID: $id" 156 | 157 | chmod +x "$dist_path" 158 | 159 | # shellcheck disable=SC2154 160 | if [[ $_devel_enable_publish == 1 ]]; then 161 | if [[ $test_error_count -gt 0 ]]; then 162 | atfile.die "Unable to publish ($test_error_count errors detected)" 163 | fi 164 | 165 | atfile.say "---\n✨ Updating..." 166 | atfile.auth "$_devel_dist_username" "$_devel_dist_password" 167 | [[ $_version == *"+"* ]] && atfile.die "Cannot publish a Git version ($_version)" 168 | 169 | atfile.say "↳ Uploading '$dist_path'..." 170 | atfile.invoke.upload "$dist_path" "" "$version_record_id" 171 | # shellcheck disable=SC2181 172 | [[ $? != 0 ]] && atfile.die "Unable to upload '$dist_path'" 173 | 174 | latest_release_record="{ 175 | \"version\": \"$_version\", 176 | \"releasedAt\": \"$(atfile.util.get_date "$commit_date")\", 177 | \"commit\": \"$commit_hash\", 178 | \"id\": \"$id\", 179 | \"checksum\": \"$checksum\" 180 | }" 181 | 182 | atfile.say "↳ Bumping current version..." 183 | # shellcheck disable=SC2154 184 | atfile.invoke.manage_record put "at://$_devel_dist_username/self.atfile.latest/self" "$latest_release_record" &> /dev/null 185 | fi 186 | } 187 | -------------------------------------------------------------------------------- /src/shared/invoke.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.invoke() { 4 | command="$1" 5 | shift 6 | args=("$@") 7 | 8 | if [[ $_is_sourced == 0 ]] && [[ $ATFILE_DEVEL_NO_INVOKE != 1 ]]; then 9 | atfile.say.debug "Invoking '$command'...\n↳ Arguments: ${args[*]}" 10 | 11 | case "$command" in 12 | "ai") 13 | atfile.ai 14 | ;; 15 | "blob") 16 | case "${args[0]}" in 17 | "list"|"ls"|"l") atfile.invoke.blob_list "${args[1]}" ;; 18 | "upload"|"u") atfile.invoke.blob_upload "${args[1]}" ;; 19 | *) atfile.die.unknown_command "$(echo "$command ${args[0]}" | xargs)" ;; 20 | esac 21 | ;; 22 | "bsky") 23 | if [[ -z "${args[0]}" ]]; then 24 | atfile.util.override_actor "$_username" 25 | atfile.bsky_profile "$_username" 26 | else 27 | atfile.bsky_profile "${args[0]}" 28 | fi 29 | ;; 30 | "cat") 31 | [[ -z "${args[0]}" ]] && atfile.die " not set" 32 | if [[ -n "${args[1]}" ]]; then 33 | atfile.util.override_actor "${args[1]}" 34 | fi 35 | 36 | atfile.invoke.print "${args[0]}" 37 | ;; 38 | "delete") 39 | [[ -z "${args[0]}" ]] && atfile.die " not set" 40 | atfile.invoke.delete "${args[0]}" 41 | ;; 42 | "fetch") 43 | [[ -z "${args[0]}" ]] && atfile.die " not set" 44 | if [[ -n "${args[1]}" ]]; then 45 | atfile.util.override_actor "${args[1]}" 46 | fi 47 | 48 | atfile.invoke.download "${args[0]}" 49 | ;; 50 | "fetch-crypt") 51 | atfile.util.check_prog_gpg 52 | [[ -z "${args[0]}" ]] && atfile.die " not set" 53 | if [[ -n "${args[1]}" ]]; then 54 | atfile.util.override_actor "${args[1]}" 55 | fi 56 | 57 | atfile.invoke.download "${args[0]}" 1 58 | ;; 59 | "handle") 60 | uri="${args[0]}" 61 | protocol="$(atfile.util.get_uri_segment "$uri" protocol)" 62 | 63 | if [[ $protocol == "https" ]]; then 64 | http_uri="$uri" 65 | uri="$(atfile.util.map_http_to_at "$http_uri")" 66 | 67 | atfile.say.debug "Mapping '$http_uri'..." 68 | 69 | if [[ -z "$uri" ]]; then 70 | atfile.die "Unable to map '$http_uri' to at:// URI" 71 | else 72 | protocol="$(atfile.util.get_uri_segment "$uri" protocol)" 73 | fi 74 | fi 75 | 76 | atfile.say.debug "Handling protocol '$protocol://'..." 77 | 78 | case $protocol in 79 | "at") atfile.invoke.handle_aturi "$uri" ;; 80 | "atfile") atfile.invoke.handle_atfile "$uri" "${args[1]}" ;; 81 | esac 82 | ;; 83 | "help") 84 | atfile.help 85 | ;; 86 | "info") 87 | [[ -z "${args[0]}" ]] && atfile.die " not set" 88 | if [[ -n "${args[1]}" ]]; then 89 | atfile.util.override_actor "${args[1]}" 90 | fi 91 | 92 | atfile.invoke.get "${args[0]}" 93 | ;; 94 | "install") 95 | # TODO: Disable when installed (how?), similar to `release` 96 | atfile.install "${args[0]}" "${args[1]}" "${args[2]}" 97 | ;; 98 | "list") 99 | if [[ "${args[0]}" == *.* || "${args[0]}" == did:* ]]; then 100 | # NOTE: User has entered in the wrong place, so we'll fix it 101 | # for them 102 | # BUG: Keys with periods in them can't be used as a cursor 103 | 104 | atfile.util.override_actor "${args[0]}" 105 | 106 | atfile.invoke.list "${args[1]}" 107 | else 108 | if [[ -n "${args[1]}" ]]; then 109 | atfile.util.override_actor "${args[1]}" 110 | fi 111 | atfile.invoke.list "${args[0]}" 112 | fi 113 | ;; 114 | "lock") 115 | atfile.invoke.lock "${args[0]}" 1 116 | ;; 117 | "now") 118 | atfile.now "${args[0]}" 119 | ;; 120 | "record") 121 | # NOTE: Performs no validation (apart from JSON)! Here be dragons 122 | case "${args[0]}" in 123 | "add"|"create"|"c") atfile.record "create" "${args[1]}" "${args[2]}" ;; 124 | "get"|"g") atfile.record "get" "${args[1]}" ;; 125 | "ls"|"list"|"l") atfile.record_list "${args[1]}" ;; 126 | "put"|"update"|"u") atfile.record "update" "${args[1]}" "${args[2]}" ;; 127 | "rc"|"recreate"|"r") atfile.record "recreate" "${args[1]}" "${args[2]}" ;; 128 | "rm"|"delete"|"d") atfile.record "delete" "${args[1]}" ;; 129 | *) atfile.die.unknown_command "$(echo "$command ${args[0]}" | xargs)" ;; 130 | esac 131 | ;; 132 | "release") 133 | if [[ $ATFILE_DEVEL == 1 ]]; then 134 | atfile.release 135 | else 136 | atfile.die.unknown_command "$command" 137 | fi 138 | ;; 139 | "resolve") 140 | atfile.resolve "${args[0]}" 141 | ;; 142 | "something-broke") 143 | atfile.something_broke 144 | ;; 145 | "stream") 146 | atfile.stream "${args[0]}" "${args[1]}" "${args[2]}" "${args[3]}" 147 | ;; 148 | "token") 149 | atfile.invoke.token 150 | ;; 151 | "toggle-mime") 152 | atfile.invoke.toggle_desktop 153 | ;; 154 | "upload") 155 | atfile.util.check_prog_optional_metadata 156 | [[ -z "${args[0]}" ]] && atfile.die " not set" 157 | atfile.invoke.upload "${args[0]}" "" "${args[1]}" 158 | ;; 159 | "upload-crypt") 160 | atfile.util.check_prog_optional_metadata 161 | atfile.util.check_prog_gpg 162 | [[ -z "${args[0]}" ]] && atfile.die " not set" 163 | [[ -z "${args[1]}" ]] && atfile.die " not set" 164 | atfile.invoke.upload "${args[0]}" "${args[1]}" "${args[2]}" 165 | ;; 166 | "unlock") 167 | atfile.invoke.lock "${args[0]}" 0 168 | ;; 169 | "update") 170 | atfile.update install 171 | ;; 172 | "url") 173 | [[ -z "${args[0]}" ]] && atfile.die " not set" 174 | if [[ -n "${args[1]}" ]]; then 175 | atfile.util.override_actor "${args[1]}" 176 | fi 177 | 178 | atfile.invoke.get_url "${args[0]}" 179 | ;; 180 | "version") 181 | echo -e "$_version" 182 | atfile.cache.del "update-check" 183 | atfile.update check-only 184 | ;; 185 | *) 186 | atfile.die.unknown_command "$command" 187 | ;; 188 | esac 189 | 190 | atfile.update check-only 191 | fi 192 | } 193 | -------------------------------------------------------------------------------- /src/commands/help.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.help() { 4 | # shellcheck disable=SC2154 5 | if [[ $_output_json == 1 ]]; then 6 | atfile.die "Command not available as JSON" 7 | fi 8 | 9 | unset error 10 | unset handle 11 | 12 | resolved_did="$(atfile.util.resolve_identity "$_meta_did")" 13 | error="$(atfile.util.get_xrpc_error $? "$resolved_did")" 14 | 15 | if [[ -n "$error" ]]; then 16 | handle="invalid.handle" 17 | else 18 | handle="$(echo "$resolved_did" | cut -d "|" -f 3 | sed -s 's/at:\/\///g')" 19 | fi 20 | 21 | # ------------------------------------------------------------------------------ 22 | usage_commands="Commands 23 | upload [] 24 | Upload new file to the PDS 25 | ⚠️ ATProto records are public: do not upload sensitive files\n 26 | list [] [] 27 | list [] 28 | List all uploaded files. Only $_max_list items can be displayed; to 29 | paginate, use the last Key for \n 30 | fetch [] 31 | Download an uploaded file\n 32 | cat [] 33 | Print (don't download) an uploaded file to the shell\n 34 | url [] 35 | Get blob URL for an uploaded file\n 36 | info [] 37 | Get full details for an uploaded file\n 38 | delete 39 | Delete an uploaded file 40 | ⚠️ No confirmation is asked before deletion\n 41 | lock 42 | unlock 43 | Lock (or unlock) an uploaded file to prevent it from unintended 44 | deletions 45 | ⚠️ Other clients may be able to delete the file. This is intended as 46 | a safety-net to avoid inadvertently deleting the wrong file\n 47 | upload-crypt [] 48 | Encrypt file (with GPG) for and upload to the PDS 49 | ℹ️ Make sure the necessary GPG key has been imported first\n 50 | fetch-crypt [] 51 | Download an uploaded encrypted file and attempt to decrypt it (with 52 | GPG) 53 | ℹ️ Make sure the necessary GPG key has been imported first" 54 | 55 | usage_commands_devel="Commands (Devel) 56 | build 57 | Build (and release, if requested) as one file (to ./bin) 58 | ℹ️ Set ${_envvar_prefix}_DEVEL_ENABLE_PUBLISH=1 to upload release" 59 | 60 | usage_commands_lifecycle="Commands (Lifecycle) 61 | update 62 | Check for updates and update if outdated 63 | ⚠️ If installed from your system's package manager, self-updating is 64 | not possible\n 65 | toggle-mime 66 | Install/uninstall desktop file to handle atfile:/at: protocol" 67 | 68 | usage_commands_tools="Commands (Tools) 69 | blob list 70 | List blobs on authenticated repository\n 71 | blob upload 72 | Upload blobs to authenticated repository 73 | ℹ️ Unless referenced by a record shortly after uploading, blob will be 74 | garbage collected by the PDS\n 75 | bsky [] 76 | Get Bluesky profile for \n 77 | handle 78 | Open at:// URI with relevant App\n 79 | handle [] 80 | Open atfile:// URI with relevant App 81 | ℹ️ Set to a .desktop entry (with '.desktop') to force the 82 | application opens with\n 83 | now 84 | Get date in ISO-8601 format\n 85 | record list 86 | record create 87 | record create 88 | record get 89 | record update 90 | record recreate 91 | record delete 92 | Manage records on a repository 93 | ⚠️ No validation is performed. Here be dragons!\n 94 | resolve 95 | Get details for \n 96 | stream [] [] [] [] 97 | Stream records from Jetstream 98 | ℹ️ For multiple values (where appropriate) separate with ';'\n 99 | token 100 | Get JWT for authenticated account" 101 | 102 | usage_envvars="Environment Variables 103 | ${_envvar_prefix}_USERNAME (required) 104 | Username of the PDS user (handle or DID) 105 | ${_envvar_prefix}_PASSWORD (required) 106 | Password of the PDS user 107 | ℹ️ An App Password is recommended 108 | ($_endpoint_social_app/settings/app-passwords)\n 109 | ${_envvar_prefix}_ENABLE_FINGERPRINT (default: $_enable_fingerprint_default) 110 | Apply machine fingerprint to uploaded files 111 | ${_envvar_prefix}_OUTPUT_JSON (default: $_output_json_default) 112 | Print all commands (and errors) as JSON 113 | ⚠️ When sourcing, sets to 1 114 | ${_envvar_prefix}_MAX_LIST (default: $_max_list_default) 115 | Maximum amount of items in any lists 116 | ℹ️ Default value is calculated from your terminal's height 117 | ⚠️ When output is JSON (${_envvar_prefix}_OUTPUT_JSON=1), sets to 100 118 | ${_envvar_prefix}_FMT_BLOB_URL (default: $_fmt_blob_url_default) 119 | Format for blob URLs. Fragments: 120 | * [server]: PDS endpoint 121 | * [did]: Actor DID 122 | * [cid]: Blob CID 123 | ${_envvar_prefix}_FMT_OUT_FILE (default: $_fmt_out_file_default) 124 | Format for fetched filenames. Fragments: 125 | * [key]: Record key of uploaded file 126 | * [name]: Original name of uploaded file\n 127 | ${_envvar_prefix}_ENDPOINT_APPVIEW (default: ${_endpoint_appview_default}$([[ $_endpoint_appview_default == *"zio.blue" ]] && echo "²")) 128 | Endpoint of the Bluesky (or compatible) AppView 129 | ${_envvar_prefix}_ENDPOINT_JETSTREAM (default: $_endpoint_jetstream_default$([[ $_endpoint_jetstream_default == *"zio.blue" ]] && echo "²")) 130 | Endpoint of the Jetstream relay 131 | ${_envvar_prefix}_ENDPOINT_PDS 132 | Endpoint of the PDS 133 | ℹ️ Your PDS is resolved from your username. Set to override it (or if 134 | resolving fails) 135 | ${_envvar_prefix}_ENDPOINT_PLC_DIRECTORY (default: ${_endpoint_plc_directory_default}$([[ $_endpoint_plc_directory_default == *"zio.blue" ]] && echo "²")) 136 | Endpoint of the PLC directory 137 | ${_envvar_prefix}_ENDPOINT_SOCIAL_APP (default: ${_endpoint_social_app_default}) 138 | Endpoint of the Bluesky (or compatible) social app\n 139 | ${_envvar_prefix}_DISABLE_AUTH_CHECK (default: $_disable_auth_check_default) 140 | Skip session validation on startup 141 | If you're confident your credentials are correct, and 142 | \$${_envvar_prefix}_USERNAME is a DID (*not* a handle), this will 143 | drastically improve performance! 144 | ${_envvar_prefix}_DISABLE_NI_EXIFTOOL (default: $_disable_ni_exiftool_default) 145 | Do not check if ExifTool is installed 146 | ⚠️ If Exiftool is not installed, the relevant metadata records will 147 | not be created: 148 | * image/*: $_nsid_meta#photo 149 | ${_envvar_prefix}_DISABLE_NI_MD5SUM (default: $_disable_ni_md5sum_default) 150 | Do not check if MD5Sum is installed 151 | ${_envvar_prefix}_DISABLE_NI_MEDIAINFO (default: $_disable_ni_mediainfo_default) 152 | Do not check if MediaInfo is installed 153 | ⚠️ If MediaInfo is not installed, the relevant metadata records will 154 | not be created: 155 | * audio/*: $_nsid_meta#audio 156 | * video/*: $_nsid_meta#video 157 | ${_envvar_prefix}_DISABLE_SETUP_DIR_CREATION (default: $_disable_setup_dir_creation) 158 | Disable directory creation during setup 159 | ${_envvar_prefix}_DISABLE_UNSUPPORTED_OS_WARN (default: $_disable_unsupported_os_warn) 160 | Do not error when running on an unsupported OS 161 | ${_envvar_prefix}_DISABLE_UPDATE_CHECKING (default: $_disable_update_checking_default) 162 | Disable periodic update checking when command finishes 163 | ${_envvar_prefix}_DISABLE_UPDATE_COMMAND (default: $_disable_update_command_default) 164 | Disable \`update\` command\n 165 | ${_envvar_prefix}_DEBUG (default: 0) 166 | Print debug outputs 167 | ⚠️ When output is JSON (${_envvar_prefix}_OUTPUT_JSON=1), sets to 0\n 168 | ¹ A bool in Bash is 1 (true) or 0 (false) 169 | ² These servers are ran by @$handle. You can trust us!" 170 | 171 | usage_paths="Paths 172 | $_path_envvar 173 | List of key/values of the above environment variables. Exporting these 174 | on the shell (with \`export \$ATFILE_VARIABLE\`) overrides these values 175 | ℹ️ Set ${_envvar_prefix}_PATH_CONF to override\n 176 | $_path_cache/ 177 | $_path_blobs_tmp/ 178 | Cache and temporary storage" 179 | 180 | usage="ATFile" 181 | [[ $_os != "haiku" ]] && usage+=" | 📦 ➔ 🦋" 182 | 183 | usage+="\n Store and retrieve files on the ATmosphere\n 184 | Version $_version 185 | (c) $_meta_year $_meta_author <$_meta_repo> 186 | Licensed as MIT License ✨\n 187 | 🦋 Follow @$handle on the ATmosphere 188 | ↳ $_endpoint_social_app_name: $_endpoint_social_app/profile/$handle 189 | ↳ Tangled: https://tangled.sh/@$handle 190 | 191 | Usage 192 | $_prog [] 193 | $_prog at://[//] 194 | $_prog atfile:///\n\n" 195 | 196 | [[ $ATFILE_DEVEL == 1 ]] && usage+="$usage_commands_devel\n\n" 197 | usage+="$usage_commands\n\n" 198 | usage+="$usage_commands_lifecycle\n\n" 199 | usage+="$usage_commands_tools\n\n" 200 | usage+="$usage_envvars\n\n" 201 | usage+="$usage_paths\n" 202 | 203 | if [[ $_debug == 1 ]]; then 204 | echo -e "$usage" 205 | else 206 | echo -e "$usage" | less 207 | fi 208 | 209 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to resolve '$_meta_did'" "$error" 210 | 211 | # ------------------------------------------------------------------------------ 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ATFile 3 |

4 | 5 |

6 | Store and retrieve files on the ATmosphere (like Bluesky or Tangled)
7 | Written entirely in Bash Shell. No NodeJS here! 8 |

9 | 10 |
11 | 12 | ## ✨ Quick Start 13 | 14 | ```sh 15 | curl -sSL https://zio.sh/atfile.sh | bash 16 | echo 'ATFILE_USERNAME=""' > ~/.config/atfile.env # e.g. alice.bsky.social, did:plc:wshs7t2adsemcrrd4snkeqli, did:web:zio.sh 17 | echo 'ATFILE_PASSWORD=""' >> ~/.config/atfile.env 18 | atfile help 19 | ``` 20 | 21 | ## 👀 Detailed Usage 22 | 23 | ### ✅ Requirements 24 | 25 | * **OS¹** 26 | * 🟡 **Linux**: GNU, MinGW and Termux only; musl² not supported 27 | * 🟢 **macOS**: Compatible with built-in version of Bash (3.2) 28 | * 🔴 **Windows**: No native version available 29 | * Run with MinGW (Cygwin, Git Bash, MSYS2, etc.) or WSL (see Linux caveats above) 30 | * 🟢 __*BSD__: FreeBSD, NetBSD, OpenBSD, and other *BSD's 31 | * 🟢 **Haiku**: [Yes, really](https://bsky.app/profile/did:plc:kv7sv4lynbv5s6gdhn5r5vcw/post/3lboqznyqgs26) 32 | * 🔴 **Solaris**: Has issues; low priority 33 | * 🔴 **SerenityOS**: Untested 34 | * **Bash³:** 3.x or later 35 | * **Packages** 36 | * [`curl`](https://curl.se) 37 | * [ExifTool (`exiftool`)](https://exiftool.org) _(optional: set `ATFILE_DISABLE_NI_EXIFTOOL=1` to ignore)_ 38 | * [`file`](https://www.darwinsys.com/file) _(only on *BSD, macOS, or Linux)_ 39 | * [GnuPG (`gpg`)](https://gnupg.org) _(optional: needed for `upload-crypt`, `fetch-crypt`)_ 40 | * [`jq`](https://jqlang.github.io/jq) 41 | * [MediaInfo (`mediainfo`)](https://mediaarea.net/en/MediaInfo) _(optional: set `ATFILE_DISABLE_NI_MEDIAINFO=1` to ignore)_ 42 | * `md5sum` _(optional: set `ATFILE_DISABLE_NI_MD5SUM=1` to ignore)_ 43 | * Both GNU and BusyBox versions supported 44 | * [`websocat`](https://github.com/vi/websocat) _(optional: needed for `stream`)_ 45 | * **ATProto account** 46 | * Limit the amount of files you upload, and avoid copyrighted files, if using a managed PDS
(e.g. [Blacksky](https://pds.blacksky.app), [Bluesky](https://bsky.social), [Spark](https://pds.sprk.so), [Tangled](https://tngl.sh), or any other independent PDS you don't own) 47 | * Supports accounts with `did:plc` and `did:web` identities 48 | * Supports PDSs running [Bluesky PDS](https://github.com/bluesky-social/pds) and [millipds](https://github.com/DavidBuchanan314/millipds) 49 | * Other PDSs remain untested, but if they implement standard `com.atproto.*` endpoints, there should be no reason these won't work 50 | * Filesize limits cannot be automatically detected. By default, this is 100MB 51 | * To change this on Bluesky PDS, set `PDS_BLOB_UPLOAD_LIMIT=` 52 | * If the PDS is running behind Cloudflare, the Free plan imposes a 100MB upload limit 53 | * This tool, nor setting a higher filesize limit, **does not workaround [video upload limits on Bluesky](https://bsky.social/about/blog/09-11-2024-video).** Videos are served via a [CDN](https://video.bsky.app), and adding larger videos to post records yields errors 54 | 55 | ### ⬇️ Downloading & Installing 56 | 57 | There are three ways of installing ATFile. Either: 58 | 59 | #### Automatic ("`curl|bash`") 60 | 61 | ``` 62 | curl -sSL https://zio.sh/atfile.sh | bash 63 | ``` 64 | 65 | This will automatically fetch the latest version of ATFile and install it in an appropriate location, as well as creating a blank configuration file. Once downloaded and installed, the locations used will be output. They are as follows: 66 | 67 | * __Linux/*BSD/Solaris/SerenityOS__ 68 | * Install: `$HOME/.local/bin/atfile` 69 | * As `sudo`/`root`: `/usr/local/bin/atfile` 70 | * Config: `$HOME/.config/atfile.env`, **or** `$XDG_CONFIG_HOME/atfile.env` (if set) 71 | * **macOS** 72 | * Install: `$HOME/.local/bin/atfile` 73 | * As `sudo`/`root`: `/usr/local/bin/atfile` 74 | * Config: `$HOME/Library/Application Support/atfile.env` 75 | * **Haiku** 76 | * Install: `/boot/system/non-packaged/bin/atfile` 77 | * Config: `$HOME/config/settings/atfile.env` 78 | 79 | #### Manually 80 | 81 | See [tags on @zio.sh/atfile](https://tangled.org/@zio.sh/atfile/tags), and download the required version under **Artifacts** — this can be stored and run from anywhere (and is identical to the version `curl|bash` fetched. Consider renaming to `atfile.sh` (as ATFile can update itself, making a fixed version in the filename nonsensical), and mark as executable (with `chmod +x atfile.sh`). 82 | 83 | Config locations are identical to those above (see **Automatic ("`curl|bash`")** above). 84 | 85 | #### Repository 86 | 87 | If you've pulled this repository, you can also use ATFile by simply calling `./atfile.sh` — it functions just as a regular compiled version of ATFile, including reading from the same config file. Debug messages are turned on by default: disable these by setting `ATFILE_DEBUG=0`. 88 | 89 | Config locations are identical to those above (see **Automatic ("`curl|bash`")** above). 90 | 91 | **Using a development version against your ATProto account could potentially inadvertently damage records.** 92 | 93 | ### ⌨️ Using 94 | 95 | See `atfile help`. 96 | 97 | ## 🏗️ Building 98 | 99 | To compile, run `./atfile.sh build`. The built version will be available at `./bin/atfile-[+git.].sh`. 100 | 101 | ### Environment variables 102 | 103 | Various environment variables can be exported to control various aspects of the development version. These are as follows: 104 | 105 | * `ATFILE_DEVEL_ENABLE_PIPING` <int> (default: `0`)
Allow piping (useful to test installation) _(e.g. `cat ./atfile.sh | bash`)_ 106 | * `ATFILE_DEVEL_ENABLE_PUBLISH` <int> (default: `0`)
Publish build to ATProto repository (to allow for updating) as the last step when running `release`. Several requirements must be fulfilled to succeed: 107 | * `ATFILE_DEVEL_DIST_USERNAME` must be set
By default, this is set to `$did` in `atfile.sh` (see **🏗️ Building ➔ Meta**). Ideally, you should not set this variable as updates in the built version will not be fetched from the correct place 108 | * `ATFILE_DEVEL_DIST_PASSWORD` must be set 109 | * No tests should return an **Error** (**Warning** is acceptable) 110 | * Git commit must be tagged 111 | 112 | Other `ATFILE_DEVEL_` environment variables are visible in the codebase, but these are computed internally and cannot be set/modified. 113 | 114 | ### Directives 115 | 116 | Various build directives can be set in files to control various aspects of the development version. These are set with `# atfile-devel=` directive at the top of the file, using commas to separate values. These are as follows: 117 | 118 | * `ignore-build`
Do not include file in the final compiled build 119 | 120 | ### Meta 121 | 122 | Various meta variables can be set to be available in the final compiled build (usually found in `help`). These are found in `atfile.sh` under `# Meta`. These variables are as follows: 123 | 124 | * `author` <string>
Copyright author 125 | * `did` <did>
DID of copyright author. Also used as the source for published builds, unless `ATFILE_DEVEL_DIST_USERNAME` is set (see **🏗️ Building ➔ Environment variables**) 126 | * `repo` <uri>
Repository URL of source code 127 | * `version` <string>
Version in the format of `.[.]`. **Not following this format will cause unintended issues.** Git hashes (`+git.abc1234`) are added automatically during build when a git tag is **not** applied to the current commit 128 | * `year` <int>
Copyright year 129 | 130 | ## ⌨️ Contributing 131 | 132 | Development takes place on [Tangled (@zio.sh/atfile)](https://tangled.sh/@zio.sh/atfile), with [GitHub (ziodotsh/atfile)](https://github.com/ziodotsh/atfile) acting as a mirror. Use Tangled for your contributions, for both Issues and Pulls. As Tangled is powered by ATProto, you already have an account (unsure? Try logging in with your Bluesky handle). 133 | 134 | When submitting Pulls, **target the `dev` branch**: `main` is the current stable production version, and Pulls will be rejected targeting this branch. 135 | 136 | ## 🤝 Acknowledgements 137 | 138 | * **Paul Frazee** — [🦋 @pfrazee.com](https://bsky.app/profile/did:plc:ragtjsm2j2vknwkz3zp4oxrd)
His kind words 139 | * **Laurens Hof** — [🦋 @laurenshof.online](https://bsky.app/profile/did:plc:mdjhvva6vlrswsj26cftjttd)
Featuring ATFile on [The Fediverse Report](https://fediversereport.com): _["Last Week in the ATmosphere – Oct 2024 week 4"](https://fediversereport.com/last-week-in-the-atmosphere-oct-2024-week-4/)_ 140 | * **Samir** — [🐙 @bdotsamir](https://github.com/bdotsamir)
Testing, and diagnosing problems with, support for macOS (`macos`) 141 | * **Astra** — [🦋 @astra.blue](https://bsky.app/profile/did:plc:ejy6lkhb72rxvkk57tnrmpjl)
[Various PRs](https://github.com/ziodotsh/atfile/pulls?q=is%3Apr+author%3Aastravexton); testing, and diagnosing problems with, support for MinGW (`linux-mingw`) and Termux (`linux-termux`) 142 | * _(Forgot about you? [You know what to do](https://tangled.sh/@zio.sh/atfile/pulls/new))_ 143 | 144 | --- 145 | 146 | * **¹** You can bypass OS detection in one of two ways: 147 | * Set `ATFILE_DISABLE_UNSUPPORTED_OS_WARN=1`
Be careful! There's a reason some OSes are not supported 148 | * Set `ATFILE_FORCE_OS=`
This overrides the OS detected. Possible values: `bsd`, `haiku`, `linux`, `linux-mingw`, `linux-musl`, `linux-termux`, `macos`, `serenity`, and `solaris`. 149 | * **²** musl-powered distros do not use GNU/glibc packages, and have problems currently 150 | * Known musl distros: Alpine, Chimera, Dragora, Gentoo (musl), Morpheus, OpenWrt, postmarketOS, Sabotage, Void 151 | * Bypassing OS detection (see ¹) will cause unintended behavior 152 | * **³** As long as you have Bash installed, running from another shell will not be problematic ([`#!/usr/bin/env bash`](https://tangled.sh/@zio.sh/atfile/blob/main/atfile-install.sh#L1) forces Bash) 153 | -------------------------------------------------------------------------------- /src/lexi/blue_zio_atfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # blue.zio.atfile.* 4 | 5 | ## Records 6 | 7 | function blue.zio.atfile.meta__unknown() { 8 | reason="$1" 9 | type="$2" 10 | 11 | if [[ -z "$reason" ]]; then 12 | reason="No metadata available for $type" 13 | fi 14 | 15 | echo "{ 16 | \"\$type\": \"$_nsid_meta#unknown\", 17 | \"reason\": \"$reason\" 18 | }" 19 | } 20 | 21 | function blue.zio.atfile.meta__audio() { 22 | file="$1" 23 | 24 | if [ ! -x "$(command -v mediainfo)" ]; then 25 | blue.zio.atfile.meta__unknown "Unable to create record at time of upload (MediaInfo not installed)" 26 | return 27 | fi 28 | 29 | audio="$(atfile.util.get_mediainfo_audio_json "$file")" 30 | duration=$(atfile.util.get_mediainfo_field "$file" "General" "Duration" null) 31 | format="$(atfile.util.get_mediainfo_field "$file" "General" "Format")" 32 | tag_album="$(atfile.util.get_mediainfo_field "$file" "General" "Album")" 33 | tag_albumArtist="$(atfile.util.get_mediainfo_field "$file" "General" "Album/Performer")" 34 | tag_artist="$(atfile.util.get_mediainfo_field "$file" "General" "Performer")" 35 | tag_date="$(atfile.util.get_mediainfo_field "$file" "General" "Original/Released_Date")" 36 | tag_disc=$(atfile.util.get_mediainfo_field "$file" "General" "Part/Position" null) 37 | tag_discTotal=$(atfile.util.get_mediainfo_field "$file" "General" "Part/Position_Total" null) 38 | tag_title="$(atfile.util.get_mediainfo_field "$file" "General" "Title")" 39 | tag_track=$(atfile.util.get_mediainfo_field "$file" "General" "Track/Position" null) 40 | tag_trackTotal=$(atfile.util.get_mediainfo_field "$file" "General" "Track/Position_Total" null) 41 | 42 | parsed_tag_date="" 43 | 44 | if [[ "${#tag_date}" -gt 4 ]]; then 45 | parsed_tag_date="$(atfile.util.get_date "$tag_date")" 46 | elif [[ "${#tag_date}" == 4 ]]; then 47 | parsed_tag_date="$(atfile.util.get_date "${tag_date}-01-01")" 48 | fi 49 | 50 | echo "{ 51 | \"\$type\": \"$_nsid_meta#audio\", 52 | \"audio\": [ $audio ], 53 | \"duration\": $duration, 54 | \"format\": \"$format\", 55 | \"tags\": { 56 | \"album\": \"$tag_album\", 57 | \"album_artist\": \"$tag_albumArtist\", 58 | \"artist\": \"$tag_artist\", 59 | \"date\": $(atfile.util.get_date_json "$tag_date" "$parsed_tag_date"), 60 | \"disc\": { 61 | \"position\": $tag_disc, 62 | \"total\": $tag_discTotal 63 | }, 64 | \"title\": \"$tag_title\", 65 | \"track\": { 66 | \"position\": $tag_track, 67 | \"total\": $tag_trackTotal 68 | } 69 | } 70 | }" 71 | } 72 | 73 | function blue.zio.atfile.meta__photo() { 74 | file="$1" 75 | 76 | if [ ! -x "$(command -v exiftool)" ]; then 77 | blue.zio.atfile.meta__unknown "Unable to create record during upload (ExifTool not installed)" 78 | return 79 | fi 80 | 81 | artist="$(atfile.util.get_exiftool_field "$file" "Artist")" 82 | camera_aperture="$(atfile.util.get_exiftool_field "$file" "Aperture")" 83 | camera_exposure="$(atfile.util.get_exiftool_field "$file" "ExposureTime")" 84 | camera_flash="$(atfile.util.get_exiftool_field "$file" "Flash")" 85 | camera_focalLength="$(atfile.util.get_exiftool_field "$file" "FocalLength")" 86 | camera_iso="$(atfile.util.get_exiftool_field "$file" "ISO" null)" 87 | camera_make="$(atfile.util.get_exiftool_field "$file" "Make")" 88 | camera_mpx="$(atfile.util.get_exiftool_field "$file" "Megapixels" null)" 89 | camera_model="$(atfile.util.get_exiftool_field "$file" "Model")" 90 | date_create="$(atfile.util.get_exiftool_field "$file" "CreateDate")" 91 | date_modify="$(atfile.util.get_exiftool_field "$file" "ModifyDate")" 92 | date_tz="$(atfile.util.get_exiftool_field "$file" "OffsetTime" "+00:00")" 93 | dim_height="$(atfile.util.get_exiftool_field "$file" "ImageHeight" null)" 94 | dim_width="$(atfile.util.get_exiftool_field "$file" "ImageWidth" null)" 95 | gps_alt="$(atfile.util.get_exiftool_field "$file" "GPSAltitude" null)" 96 | gps_lat="$(atfile.util.get_exiftool_field "$file" "GPSLatitude" null)" 97 | gps_long="$(atfile.util.get_exiftool_field "$file" "GPSLongitude" null)" 98 | orientation="$(atfile.util.get_exiftool_field "$file" "Orientation")" 99 | software="$(atfile.util.get_exiftool_field "$file" "Software")" 100 | title="$(atfile.util.get_exiftool_field "$file" "Title")" 101 | 102 | date_create="$(atfile.util.parse_exiftool_date "$date_create" "$date_tz")" 103 | date_modify="$(atfile.util.parse_exiftool_date "$date_modify" "$date_tz")" 104 | 105 | [[ $gps_alt == +* ]] && gps_alt="${gps_alt:1}" 106 | [[ $gps_lat == +* ]] && gps_lat="${gps_lat:1}" 107 | [[ $gps_long == +* ]] && gps_long="${gps_long:1}" 108 | 109 | echo "{ 110 | \"\$type\": \"$_nsid_meta#photo\", 111 | \"artist\": \"$artist\", 112 | \"camera\": { 113 | \"aperture\": \"$camera_aperture\", 114 | \"device\": { 115 | \"make\": \"$camera_make\", 116 | \"model\": \"$camera_model\" 117 | }, 118 | \"exposure\": \"$camera_exposure\", 119 | \"flash\": \"$camera_flash\", 120 | \"focalLength\": \"$camera_focalLength\", 121 | \"iso\": $camera_iso, 122 | \"megapixels\": $camera_mpx 123 | }, 124 | \"date\": { 125 | \"create\": $(atfile.util.get_date_json "$date_create"), 126 | \"modify\": $(atfile.util.get_date_json "$date_modify") 127 | }, 128 | \"dimensions\": { 129 | \"height\": $dim_height, 130 | \"width\": $dim_width 131 | }, 132 | \"gps\": { 133 | \"alt\": $gps_alt, 134 | \"lat\": $gps_lat, 135 | \"long\": $gps_long 136 | }, 137 | \"orientation\": \"$orientation\", 138 | \"software\": \"$software\", 139 | \"title\": \"$title\" 140 | }" 141 | } 142 | 143 | function blue.zio.atfile.meta__video() { 144 | file="$1" 145 | 146 | if [ ! -x "$(command -v mediainfo)" ]; then 147 | blue.zio.atfile.meta__unknown "Unable to create record during upload (MediaInfo not installed)" 148 | return 149 | fi 150 | 151 | artist="$(atfile.util.get_mediainfo_field "$file" "General" "Artist")" 152 | audio="$(atfile.util.get_mediainfo_audio_json "$file")" 153 | bitRate=$(atfile.util.get_mediainfo_field "$file" "General" "BitRate" null) 154 | date_create="", 155 | date_modify="", 156 | duration=$(atfile.util.get_mediainfo_field "$file" "General" "Duration" null) 157 | format="$(atfile.util.get_mediainfo_field "$file" "General" "Format")" 158 | gps_alt=0 159 | gps_lat=0 160 | gps_long=0 161 | title="$(atfile.util.get_mediainfo_field "$file" "General" "Title")" 162 | video="$(atfile.util.get_mediainfo_video_json "$file")" 163 | 164 | if [ -x "$(command -v exiftool)" ]; then 165 | date_create="$(atfile.util.get_exiftool_field "$file" "CreateDate")" 166 | date_modify="$(atfile.util.get_exiftool_field "$file" "ModifyDate")" 167 | date_tz="$(atfile.util.get_exiftool_field "$file" "OffsetTime" "+00:00")" 168 | gps_alt="$(atfile.util.get_exiftool_field "$file" "GPSAltitude" null)" 169 | gps_lat="$(atfile.util.get_exiftool_field "$file" "GPSLatitude" null)" 170 | gps_long="$(atfile.util.get_exiftool_field "$file" "GPSLongitude" null)" 171 | 172 | date_create="$(atfile.util.parse_exiftool_date "$date_create" "$date_tz")" 173 | date_modify="$(atfile.util.parse_exiftool_date "$date_modify" "$date_tz")" 174 | fi 175 | 176 | echo "{ 177 | \"\$type\": \"$_nsid_meta#video\", 178 | \"artist\": \"$artist\", 179 | \"audio\": [ $audio ], 180 | \"biteRate\": $bitRate, 181 | \"date\": { 182 | \"create\": $(atfile.util.get_date_json "$date_create"), 183 | \"modify\": $(atfile.util.get_date_json "$date_modify") 184 | }, 185 | \"duration\": $duration, 186 | \"format\": \"$format\", 187 | \"gps\": { 188 | \"alt\": $gps_alt, 189 | \"lat\": $gps_lat, 190 | \"long\": $gps_long 191 | }, 192 | \"title\": \"$title\", 193 | \"video\": [ $video ] 194 | }" 195 | } 196 | 197 | # NOTE: Never intended to be used from ATFile. Here for reference 198 | function blue.zio.atfile.finger__browser() { 199 | url="$1" 200 | userAgent="$2" 201 | 202 | echo "{ 203 | \"\$type\": \"blue.zio.atfile.finger#browser\", 204 | \"url\": \"$url\", 205 | \"userAgent\": \"$userAgent\" 206 | }" 207 | } 208 | 209 | function blue.zio.atfile.finger__machine() { 210 | unset machine_host 211 | unset machine_id 212 | unset machine_os 213 | 214 | # shellcheck disable=SC2154 215 | if [[ $_enable_fingerprint == 1 ]]; then 216 | machine_id_file="/etc/machine-id" 217 | os_release_file="/etc/os-release" 218 | 219 | [[ -f "$machine_id_file" ]] && machine_id="$(cat "$machine_id_file")" 220 | 221 | case "$_os" in 222 | "haiku") 223 | os_version="$(uname -v | cut -d ' ' -f 1 | cut -d '+' -f 1)" 224 | 225 | case $os_version in 226 | "hrev57937") os_version="R1/Beta5" ;; 227 | esac 228 | 229 | machine_host="$(hostname)" 230 | machine_os="Haiku $os_version" 231 | ;; 232 | "linux-mingw") 233 | machine_host="$(hostname)" 234 | ;; 235 | "linux-termux") 236 | machine_os="Termux $TERMUX_VERSION" 237 | ;; 238 | "macos") 239 | os_version="$(sw_vers -productVersion | cut -d '.' -f 1,2)" 240 | 241 | case $os_version in 242 | "13."*) os_version="Ventura" ;; 243 | "14."*) os_version="Sonoma" ;; 244 | "15."*) os_version="Sequoia" ;; 245 | "26."*) os_version="Tahoe" ;; 246 | esac 247 | 248 | machine_host="$(hostname)" 249 | machine_os="macOS $os_version" 250 | ;; 251 | *) 252 | os_name="$(atfile.util.get_var_from_file "$os_release_file" "NAME")" 253 | os_version="$(atfile.util.get_var_from_file "$os_release_file" "VERSION")" 254 | 255 | machine_host="$(hostname -s)" 256 | machine_os="$os_name $os_version" 257 | ;; 258 | esac 259 | fi 260 | 261 | echo "{ 262 | \"\$type\": \"blue.zio.atfile.finger#machine\", 263 | \"app\": \"$(atfile.util.get_uas)\", 264 | \"id\": $([[ $(atfile.util.is_null_or_empty "$machine_id") == 0 ]] && echo "\"$machine_id\"" || echo "null"), 265 | \"host\": $([[ $(atfile.util.is_null_or_empty "$machine_host") == 0 ]] && echo "\"$machine_host\"" || echo "null"), 266 | \"os\": $([[ $(atfile.util.is_null_or_empty "$machine_os") == 0 ]] && echo "\"$machine_os\"" || echo "null") 267 | }" 268 | } 269 | 270 | function blue.zio.atfile.lock() { 271 | lock="$1" 272 | 273 | echo "{ 274 | \"lock\": $lock 275 | }" 276 | } 277 | 278 | function blue.zio.atfile.upload() { 279 | blob_record="$1" 280 | createdAt="$2" 281 | file_hash="$3" 282 | file_hash_type="$4" 283 | file_modifiedAt="$5" 284 | file_name="$6" 285 | file_size="$7" 286 | file_type="$8" 287 | meta_record="$9" 288 | finger_record="${10}" 289 | 290 | [[ -z $finger_record ]] && finger_record="null" 291 | [[ -z $meta_record ]] && meta_record="null" 292 | 293 | echo "{ 294 | \"createdAt\": \"$createdAt\", 295 | \"file\": { 296 | \"mimeType\": \"$file_type\", 297 | \"modifiedAt\": \"$file_modifiedAt\", 298 | \"name\": \"$file_name\", 299 | \"size\": $file_size 300 | }, 301 | \"checksum\": { 302 | \"algo\": \"$file_hash_type\", 303 | \"hash\": \"$file_hash\" 304 | }, 305 | \"finger\": $finger_record, 306 | \"meta\": $meta_record, 307 | \"blob\": $blob_record 308 | }" 309 | } 310 | -------------------------------------------------------------------------------- /src/entry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Environment 4 | 5 | ## Early-start global variables 6 | 7 | ### Permutation 8 | 9 | _start="$(atfile.util.get_date "" "%s")" # 1 10 | _envvar_prefix="ATFILE" # 2 11 | _debug="$(atfile.util.get_envvar "${_envvar_prefix}_DEBUG" "$([[ $ATFILE_DEVEL == 1 ]] && echo 1 || echo 0)")" # 3 12 | _force_os="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_OS")" # 3 13 | 14 | ### Combination 15 | 16 | _command="$1" 17 | _command_args=("${@:2}") 18 | _os="$(atfile.util.get_os)" 19 | _os_supported=0 20 | _is_piped=0 21 | _is_sourced=0 22 | _meta_author="{:meta_author:}" 23 | _meta_did="{:meta_did:}" 24 | _meta_repo="{:meta_repo:}" 25 | _meta_year="{:meta_year:}" 26 | _now="$(atfile.util.get_date)" 27 | _version="{:version:}" 28 | 29 | ## "Hello, world!" 30 | 31 | atfile.say.debug "Reticulating splines..." 32 | 33 | ## Paths 34 | 35 | _path_home="$HOME" 36 | 37 | if [[ -n "$SUDO_USER" ]]; then 38 | _path_home="$(eval echo "~$SUDO_USER")" 39 | fi 40 | 41 | _file_envvar="atfile.env" 42 | _path_blobs_tmp="/tmp" 43 | _path_cache="$_path_home/.cache" 44 | _path_envvar="$_path_home/.config" 45 | 46 | case $_os in 47 | "haiku") 48 | _path_blobs_tmp="/boot/system/cache/tmp" 49 | _path_cache="$_path_home/config/cache" 50 | _path_envvar="$_path_home/config/settings" 51 | ;; 52 | "linux-termux") 53 | _path_blobs_tmp="/data/data/com.termux/files/tmp" 54 | ;; 55 | "macos") 56 | _path_envvar="$_path_home/Library/Application Support" 57 | _path_blobs_tmp="/private/tmp" 58 | ;; 59 | esac 60 | 61 | if [[ -n "$XDG_CONFIG_HOME" ]]; then 62 | _path_envvar="$XDG_CONFIG_HOME" 63 | fi 64 | 65 | _path_blobs_tmp="$_path_blobs_tmp/at-blobs" 66 | _path_cache="$_path_cache/atfile" 67 | _path_envvar="$(atfile.util.get_envvar "${_envvar_prefix}_PATH_CONF" "$_path_envvar/$_file_envvar")" 68 | 69 | ## OS detection 70 | 71 | atfile.say.debug "Detected OS: $_os" 72 | 73 | if [[ $_os != "unknown-"* ]] &&\ 74 | [[ $_os == "bsd" ]] ||\ 75 | [[ $_os == "haiku" ]] ||\ 76 | [[ $_os == "linux" ]] ||\ 77 | [[ $_os == "linux-mingw" ]] ||\ 78 | [[ $_os == "linux-termux" ]] ||\ 79 | [[ $_os == "macos" ]] ; then 80 | _os_supported=1 81 | fi 82 | 83 | ## Pipe detection 84 | 85 | if [ -p /dev/stdin ] ||\ 86 | [[ "$0" == "bash" || $0 == *"/bin/bash" ]]; then 87 | _is_piped=1 88 | atfile.say.debug "Piping: $0" 89 | fi 90 | 91 | ## Source detection 92 | 93 | if [[ -n ${BASH_SOURCE[0]} ]]; then 94 | if [[ "$0" != "${BASH_SOURCE[0]}" ]]; then 95 | if [[ "$ATFILE_DEVEL" == 1 ]]; then 96 | if [[ -n "$ATFILE_DEVEL_SOURCE" ]]; then 97 | _is_sourced=1 98 | atfile.say.debug "Sourcing: $ATFILE_DEVEL_SOURCE" 99 | fi 100 | else 101 | _is_sourced=1 102 | atfile.say.debug "Sourcing: ${BASH_SOURCE[0]}" 103 | fi 104 | fi 105 | fi 106 | 107 | # Installation 108 | 109 | if [[ $_is_piped == 1 ]] ||\ 110 | [[ "$1" == "install" ]]; then 111 | if [[ "$1" == "install" ]]; then 112 | atfile.install "$2" "$3" "$4" 113 | install_exit="$?" 114 | else 115 | atfile.install "$1" "$2" "$3" 116 | install_exit="$?" 117 | fi 118 | 119 | atfile.util.print_seconds_since_start_debug 120 | exit $install_exit 121 | fi 122 | 123 | # Global variables 124 | 125 | ## Reflection 126 | 127 | _prog="$(basename "$(atfile.util.get_realpath "$0")")" 128 | _prog_dir="$(dirname "$(atfile.util.get_realpath "$0")")" 129 | _prog_path="$(atfile.util.get_realpath "$0")" 130 | 131 | ## Envvars 132 | 133 | ### Fallbacks 134 | 135 | _max_list_fallback=100 136 | 137 | ### Defaults 138 | 139 | _devel_dist_username_default="$_meta_did" 140 | _devel_enable_publish_default=0 141 | _disable_auth_check_default=0 142 | _disable_ni_exiftool_default=0 143 | _disable_ni_md5sum_default=0 144 | _disable_ni_mediainfo_default=0 145 | _disable_setup_dir_creation_default=0 146 | _disable_unsupported_os_warn_default=0 147 | _disable_update_checking_default=0 148 | _disable_update_command_default=0 149 | _enable_fingerprint_default=0 150 | _enable_update_git_clobber_default=0 151 | #_endpoint_appview_default="https://bsky.zio.blue" 152 | _endpoint_appview_default="https://api.bsky.app" 153 | #_endpoint_jetstream_default="wss://stream.zio.blue" 154 | _endpoint_jetstream_default="$(atfile.util.get_random_pbc_jetstream)" 155 | _endpoint_plc_directory_default="https://plc.zio.blue" 156 | _endpoint_social_app_default="https://bsky.app" 157 | _fmt_blob_url_default="[server]/xrpc/com.atproto.sync.getBlob?did=[did]&cid=[cid]" 158 | _fmt_out_file_default="[key]__[name]" 159 | _enable_fingerprint_default=0 160 | _max_list_buffer=6 161 | _max_list_default=$(( $(atfile.util.get_term_rows) - _max_list_buffer )) 162 | _output_json_default=0 163 | 164 | ### Set 165 | 166 | _devel_dist_password="$(atfile.util.get_envvar "${_envvar_prefix}_DEVEL_DIST_PASSWORD")" 167 | _devel_dist_username="$(atfile.util.get_envvar "${_envvar_prefix}_DEVEL_DIST_USERNAME" $_devel_dist_username_default)" 168 | _devel_enable_publish="$(atfile.util.get_envvar "${_envvar_prefix}_DEVEL_ENABLE_PUBLISH" $_devel_enable_publish_default)" 169 | _disable_auth_check="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_AUTH_CHECK" "$_disable_auth_check_default")" 170 | _disable_ni_exiftool="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_NI_EXIFTOOL" "$_disable_ni_exiftool_default")" 171 | _disable_ni_md5sum="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_NI_MD5SUM" "$_disable_ni_md5sum_default")" 172 | _disable_ni_mediainfo="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_NI_MEDIAINFO" "$_disable_ni_mediainfo_default")" 173 | _disable_setup_dir_creation="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_SETUP_DIR_CREATION" "$_disable_setup_dir_creation_default")" 174 | _disable_unsupported_os_warn="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_UNSUPPORTED_OS_WARN" "$_disable_unsupported_os_warn_default")" 175 | _disable_update_checking="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_UPDATE_CHECKING" $_disable_update_checking_default)" 176 | _disable_update_command="$(atfile.util.get_envvar "${_envvar_prefix}_DISABLE_UPDATE_COMMAND" $_disable_update_command_default)" 177 | _enable_fingerprint="$(atfile.util.get_envvar "${_envvar_prefix}_ENABLE_FINGERPRINT" "$_enable_fingerprint_default")" 178 | _enable_update_git_clobber="$(atfile.util.get_envvar "${_envvar_prefix}_ENABLE_UPDATE_GIT_CLOBBER" "$_enable_update_git_clobber_default")" 179 | _endpoint_appview="$(atfile.util.get_envvar "${_envvar_prefix}_ENDPOINT_APPVIEW" "$_endpoint_appview_default")" 180 | _endpoint_jetstream="$(atfile.util.get_envvar "${_envvar_prefix}_ENDPOINT_JETSTREAM" "$_endpoint_jetstream_default")" 181 | _endpoint_plc_directory="$(atfile.util.get_envvar "${_envvar_prefix}_ENDPOINT_PLC_DIRECTORY" "$_endpoint_plc_directory_default")" 182 | _endpoint_social_app="$(atfile.util.get_envvar "${_envvar_prefix}_ENDPOINT_SOCIAL_APP" "$_endpoint_social_app_default")" 183 | _fmt_blob_url="$(atfile.util.get_envvar "${_envvar_prefix}_FMT_BLOB_URL" "$_fmt_blob_url_default")" 184 | _fmt_out_file="$(atfile.util.get_envvar "${_envvar_prefix}_FMT_OUT_FILE" "$_fmt_out_file_default")" 185 | _force_meta_author="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_META_AUTHOR")" 186 | _force_meta_did="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_META_DID")" 187 | _force_meta_repo="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_META_REPO")" 188 | _force_meta_year="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_META_YEAR")" 189 | _force_now="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_NOW")" 190 | _force_version="$(atfile.util.get_envvar "${_envvar_prefix}_FORCE_VERSION")" 191 | _max_list="$(atfile.util.get_envvar "${_envvar_prefix}_MAX_LIST" "$_max_list_default")" 192 | _output_json="$(atfile.util.get_envvar "${_envvar_prefix}_OUTPUT_JSON" "$_output_json_default")" 193 | _server="$(atfile.util.get_envvar "${_envvar_prefix}_ENDPOINT_PDS")" 194 | _password="$(atfile.util.get_envvar "${_envvar_prefix}_PASSWORD")" 195 | _test_desktop_uas="Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" 196 | _username="$(atfile.util.get_envvar "${_envvar_prefix}_USERNAME")" 197 | 198 | ## Read-only 199 | 200 | _nsid_prefix="blue.zio" 201 | _nsid_lock="${_nsid_prefix}.atfile.lock" 202 | _nsid_meta="${_nsid_prefix}.atfile.meta" 203 | _nsid_upload="${_nsid_prefix}.atfile.upload" 204 | _endpoint_social_app_name="Bluesky" 205 | 206 | # Setup 207 | 208 | ## Envvar correction 209 | 210 | ### Overrides 211 | 212 | [[ -n $_force_meta_author ]] && \ 213 | _meta_author="$_force_meta_author" &&\ 214 | atfile.util.print_override_envvar_debug "Copyright Author" "_meta_author" 215 | [[ -n $_force_meta_did ]] && \ 216 | _meta_did="$_force_meta_did" &&\ 217 | _devel_dist_username="$(atfile.util.get_envvar "${_envvar_prefix}_DEVEL_DIST_USERNAME" "$_meta_did")" &&\ 218 | atfile.util.print_override_envvar_debug "DID" "_meta_did" 219 | [[ -n $_force_meta_repo ]] && \ 220 | _meta_repo="$_force_meta_repo" &&\ 221 | atfile.util.print_override_envvar_debug "Repo URL" "_meta_author" 222 | [[ -n $_force_meta_year ]] && \ 223 | _meta_year="$_force_meta_year" &&\ 224 | atfile.util.print_override_envvar_debug "Copyright Year" "_meta_year" 225 | [[ -n $_force_now ]] && \ 226 | _now="$_force_now" &&\ 227 | atfile.util.print_override_envvar_debug "Current Time" "_now" 228 | [[ -n $_force_os ]] &&\ 229 | _os="$_force_os" &&\ 230 | atfile.util.print_override_envvar_debug "OS" "_os" 231 | [[ -n $_force_version ]] && \ 232 | _version="$_force_version" &&\ 233 | atfile.util.print_override_envvar_debug "Version" "_version" 234 | 235 | if [[ $_endpoint_appview != "$_endpoint_appview_default" ]] &&\ 236 | [[ $_endpoint_social_app == "$_endpoint_social_app_default" ]]; then 237 | case "$_endpoint_appview" in 238 | "https://bsky.zeppelin.social") _endpoint_social_app="https://zeppelin.social" ;; 239 | esac 240 | fi 241 | 242 | case "$_endpoint_social_app" in 243 | "https://blacksky.community") _endpoint_social_app_name="Blacksky" ;; 244 | "https://deer.social") _endpoint_social_app_name="Deer" ;; 245 | "https://zeppelin.social") _endpoint_social_app_name="Zeppelin" ;; 246 | esac 247 | 248 | ### Validation 249 | 250 | [[ $_output_json == 1 ]] && [[ $_max_list == "$_max_list_default" ]] &&\ 251 | atfile.say.debug "Setting ${_envvar_prefix}_MAX_LIST to $_max_list_fallback\n↳ ${_envvar_prefix}_OUTPUT_JSON set to 1" &&\ 252 | _max_list=$_max_list_fallback 253 | [[ $(( _max_list > _max_list_fallback )) == 1 ]] &&\ 254 | atfile.say.debug "Setting ${_envvar_prefix}_MAX_LIST to $_max_list_fallback\n↳ Maximum is $_max_list_fallback" &&\ 255 | _max_list=$_max_list_fallback 256 | 257 | ## OS detection 258 | 259 | if [[ $_os_supported == 0 ]]; then 260 | if [[ $_disable_unsupported_os_warn == 0 ]]; then 261 | atfile.die "Unsupported OS (${_os//unknown-/})\n↳ Set ${_envvar_prefix}_DISABLE_UNSUPPORTED_OS_WARN=1 to ignore" 262 | else 263 | atfile.say.debug "Ignoring unsupported OS warning\n↳ ${_envvar_prefix}_DISABLE_UNSUPPORTED_OS_WARN set to '$_disable_unsupported_os_warn'" 264 | fi 265 | fi 266 | 267 | ## Directory creation 268 | 269 | if [[ $_disable_setup_dir_creation == 0 ]]; then 270 | atfile.util.create_dir "$_path_cache" 271 | atfile.util.create_dir "$_path_blobs_tmp" 272 | fi 273 | 274 | ## Program detection 275 | 276 | _prog_hint_jq="https://jqlang.github.io/jq" 277 | 278 | [[ "$_os" == "haiku" ]] && _prog_hint_jq="pkgman install jq" 279 | 280 | atfile.util.check_prog "curl" "https://curl.se" 281 | [[ $_os != "haiku" && $_os != "solaris" ]] && atfile.util.check_prog "file" "https://www.darwinsys.com/file" 282 | atfile.util.check_prog "jq" "$_prog_hint_jq" 283 | [[ $_disable_ni_md5sum == 0 ]] && atfile.util.check_prog "md5sum" "" "${_envvar_prefix}_DISABLE_NI_MD5SUM" 284 | #[[ $_os == "haiku" ]] && atfile.util.check_prog "perl" 285 | 286 | # Main 287 | 288 | ## Command aliases 289 | 290 | if [[ $_is_sourced == 0 ]]; then 291 | previous_command="$_command" 292 | 293 | case "$_command" in 294 | "open"|"print"|"c") _command="cat" ;; 295 | "rm") _command="delete" ;; 296 | "download"|"f"|"d") _command="fetch" ;; 297 | "download-crypt"|"fc"|"dc") _command="fetch-crypt" ;; 298 | "at") _command="handle" ;; 299 | "--help"|"-h") _command="help" ;; 300 | "get"|"i") _command="info" ;; 301 | "ls") _command="list" ;; 302 | "build") _command="release" ;; 303 | "did") _command="resolve" ;; 304 | "sb") _command="something-broke" ;; 305 | "js") _command="stream" ;; 306 | "ul"|"u") _command="upload" ;; 307 | "ub") _command="upload-blob" ;; 308 | "uc") _command="upload-crypt" ;; 309 | "--update"|"-U") _command="update" ;; 310 | "get-url"|"b") _command="url" ;; 311 | "--version"|"-V") _command="version" ;; 312 | esac 313 | 314 | if [[ $previous_command != "$_command" ]]; then 315 | atfile.say.debug "Using command '$_command' for '$previous_command'..." 316 | fi 317 | fi 318 | 319 | ## Defaults 320 | 321 | [[ $_is_sourced == 0 && -z $_command ]] && _command="help" 322 | 323 | if [[ "$_command" == "atfile:"* || "$_command" == "at:"* || "$_command" == "https:"* ]]; then 324 | _command="handle" 325 | _command_args=("$1") 326 | atfile.say.debug "Handling '${_command_args[*]}'..." 327 | fi 328 | 329 | ## Invoke 330 | 331 | if [[ $_is_sourced == 0 ]]; then 332 | atfile.auth 333 | atfile.invoke "$_command" "${_command_args[@]}" 334 | fi 335 | 336 | atfile.util.print_seconds_since_start_debug 337 | -------------------------------------------------------------------------------- /src/commands/old_cmds.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function atfile.invoke.blob_list() { 4 | cursor="$1" 5 | unset error 6 | 7 | atfile.say.debug "Getting blobs...\n↳ Repo: $_username" 8 | blobs="$(com.atproto.sync.listBlobs "$_username" "$cursor")" 9 | error="$(atfile.util.get_xrpc_error $? "$blobs")" 10 | 11 | if [[ -z "$error" ]]; then 12 | records="$(echo "$blobs" | jq -c '.cids[]')" 13 | if [[ -z "$records" ]]; then 14 | if [[ -n "$cursor" ]]; then 15 | atfile.die "No more blobs for '$_username'" 16 | else 17 | atfile.die "No blobs for '$_username'" 18 | fi 19 | fi 20 | 21 | unset first_cid 22 | unset last_cid 23 | unset browser_accessible 24 | unset record_count 25 | unset json_output 26 | 27 | # shellcheck disable=SC2154 28 | if [[ $_output_json == 0 ]]; then 29 | echo -e "Blob" 30 | echo -e "----" 31 | else 32 | json_output="{\"blobs\":[" 33 | fi 34 | 35 | while IFS=$'\n' read -r c; do 36 | cid="$(echo "$c" | jq -r ".")" 37 | blob_uri="$(atfile.util.build_blob_uri "$_username" "$cid")" 38 | last_cid="$cid" 39 | ((record_count++)) 40 | 41 | if [[ -z $first_cid ]]; then 42 | first_cid="$cid" 43 | browser_accessible=$(atfile.util.is_url_accessible_in_browser "$blob_uri") 44 | fi 45 | 46 | if [[ -n $cid ]]; then 47 | if [[ $_output_json == 1 ]]; then 48 | json_output+="{ \"cid\": \"$cid\", \"url\": \"$blob_uri\" }," 49 | else 50 | if [[ $browser_accessible == 1 ]]; then 51 | echo "$blob_uri" 52 | else 53 | echo "$cid" 54 | fi 55 | fi 56 | fi 57 | done <<< "$records" 58 | 59 | if [[ $_output_json == 0 ]]; then 60 | atfile.util.print_table_paginate_hint "$last_cid" $record_count 61 | else 62 | json_output="${json_output::-1}" 63 | json_output+="]," 64 | json_output+="\"browser_accessible\": $(atfile.util.get_yn "$browser_accessible")," 65 | json_output+="\"cursor\": \"$last_cid\"}" 66 | echo -e "$json_output" | jq 67 | fi 68 | else 69 | atfile.die "Unable to list blobs" 70 | fi 71 | } 72 | 73 | function atfile.invoke.blob_upload() { 74 | file="$1" 75 | 76 | if [[ ! -f "$file" ]]; then 77 | atfile.die "File '$file' does not exist" 78 | else 79 | file="$(atfile.util.get_realpath "$file")" 80 | fi 81 | 82 | atfile.say.debug "Uploading blob...\n↳ File: $file" 83 | com.atproto.sync.uploadBlob "$file" | jq 84 | } 85 | 86 | function atfile.invoke.delete() { 87 | key="$1" 88 | success=1 89 | unset error 90 | 91 | lock_record="$(com.atproto.repo.getRecord "$_username" "blue.zio.atfile.lock" "$key")" 92 | 93 | if [[ $(echo "$lock_record" | jq -r ".value.lock") == true ]]; then 94 | # shellcheck disable=SC2154 95 | atfile.die "Unable to delete '$key' — file is locked\n Run \`$_prog unlock $key\` to unlock file" 96 | fi 97 | 98 | # shellcheck disable=SC2154 99 | record="$(com.atproto.repo.deleteRecord "$_username" "$_nsid_upload" "$key")" 100 | error="$(atfile.util.get_xrpc_error $? "$record")" 101 | 102 | if [[ -z "$error" ]]; then 103 | if [[ $_output_json == 1 ]]; then 104 | echo "{ \"deleted\": true }" | jq 105 | else 106 | echo "Deleted: $key" 107 | fi 108 | else 109 | atfile.die.xrpc_error "Unable to delete '$key'" "$error" 110 | fi 111 | } 112 | 113 | function atfile.invoke.download() { 114 | key="$1" 115 | decrypt=$2 116 | success=1 117 | downloaded_file="" 118 | 119 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 120 | record="$(com.atproto.repo.getRecord "$_username" "$_nsid_upload" "$key")" 121 | [[ $? != 0 || -z "$record" || "$record" == "{}" || "$record" == *"\"error\":"* ]] && success=0 122 | 123 | if [[ $success == 1 ]]; then 124 | blob_uri="$(atfile.util.build_blob_uri "$(echo "$record" | jq -r ".uri" | cut -d "/" -f 3)" "$(echo "$record" | jq -r ".value.blob.ref.\"\$link\"")")" 125 | file_name="$(echo "$record" | jq -r '.value.file.name')" 126 | key="$(atfile.util.get_rkey_from_at_uri "$(echo "$record" | jq -r ".uri")")" 127 | downloaded_file="$(atfile.util.build_out_filename "$key" "$file_name")" 128 | 129 | curl -H "User-Agent: $(atfile.util.get_uas)" --silent "$blob_uri" -o "$downloaded_file" 130 | # shellcheck disable=SC2181 131 | [[ $? != 0 ]] && success=0 132 | fi 133 | 134 | if [[ $decrypt == 1 && $success == 1 ]]; then 135 | new_downloaded_file="$(echo "$downloaded_file" | sed -s s/.gpg//)" 136 | 137 | gpg --quiet --output "$new_downloaded_file" --decrypt "$downloaded_file" 138 | 139 | # shellcheck disable=SC2181 140 | if [[ $? != 0 ]]; then 141 | success=0 142 | else 143 | rm -f "$downloaded_file" 144 | downloaded_file="$new_downloaded_file" 145 | fi 146 | fi 147 | 148 | if [[ $success == 1 ]]; then 149 | if [[ $_output_json == 1 ]]; then 150 | is_decrypted="false" 151 | [[ $decrypt == 1 ]] && is_decrypted="true" 152 | echo -e "{ \"decrypted\": $is_decrypted, \"name\": \"$(basename "${downloaded_file}")\", \"path\": \"$(atfile.util.get_realpath "${downloaded_file}")\" }" | jq 153 | else 154 | echo -e "Downloaded: $key" 155 | [[ $decrypt == 1 ]] && echo "Decrypted: $downloaded_file" 156 | echo -e "↳ Path: $(atfile.util.get_realpath "$downloaded_file")" 157 | fi 158 | else 159 | [[ -f "$downloaded_file" ]] && rm -f "$downloaded_file" 160 | atfile.die "Unable to download '$key'" 161 | fi 162 | } 163 | 164 | function atfile.invoke.get() { 165 | key="$1" 166 | success=1 167 | 168 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 169 | record="$(com.atproto.repo.getRecord "$_username" "$_nsid_upload" "$key")" 170 | [[ $? != 0 || -z "$record" || "$record" == "{}" || "$record" == *"\"error\":"* ]] && success=0 171 | 172 | if [[ $success == 1 ]]; then 173 | file_type="$(echo "$record" | jq -r '.value.file.mimeType')" 174 | did="$(echo "$record" | jq -r ".uri" | cut -d "/" -f 3)" 175 | key="$(atfile.util.get_rkey_from_at_uri "$(echo "$record" | jq -r ".uri")")" 176 | blob_uri="$(atfile.util.build_blob_uri "$did" "$(echo "$record" | jq -r ".value.blob.ref.\"\$link\"")")" 177 | cdn_uri="$(atfile.util.get_cdn_uri "$did" "$(echo "$record" | jq -r ".value.blob.ref.\"\$link\"")" "$file_type")" 178 | 179 | unset locked 180 | unset encrypted 181 | 182 | # shellcheck disable=SC2154 183 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_lock\n↳ Repo: $_username\n↳ Key: $key" 184 | locked_record="$(com.atproto.repo.getRecord "$_username" "$_nsid_lock" "$key")" 185 | # shellcheck disable=SC2181 186 | if [[ $? == 0 ]] && [[ -n "$locked_record" ]]; then 187 | if [[ $(echo "$locked_record" | jq -r ".value.lock") == true ]]; then 188 | locked="$(atfile.util.get_yn 1)" 189 | else 190 | locked="$(atfile.util.get_yn 0)" 191 | fi 192 | fi 193 | 194 | if [[ "$file_type" == "application/prs.atfile.gpg-crypt" ]]; then 195 | encrypted="$(atfile.util.get_yn 1)" 196 | else 197 | encrypted="$(atfile.util.get_yn 0)" 198 | fi 199 | 200 | if [[ $_output_json == 1 ]]; then 201 | browser_accessible=$(atfile.util.get_yn "$(atfile.util.is_url_accessible_in_browser "$blob_uri")") 202 | 203 | echo "{ \"encrypted\": $encrypted, \"locked\": $locked, \"upload\": $(echo "$record" | jq -r ".value"), \"url\": { \"blob\": \"$blob_uri\", \"browser_accessible\": $browser_accessible, \"cdn\": { \"bsky\": \"$cdn_uri\" } } }" | jq 204 | else 205 | file_date="$(echo "$record" | jq -r '.value.file.modifiedAt')" 206 | file_hash="$(echo "$record" | jq -r '.value.checksum.hash')" 207 | file_hash_type="$(echo "$record" | jq -r '.value.checksum.algo')" 208 | [[ "$file_hash_type" == "null" ]] && file_hash_type="$(echo "$record" | jq -r '.value.checksum.type')" 209 | file_hash_pretty="$file_hash ($file_hash_type)" 210 | file_name="$(echo "$record" | jq -r '.value.file.name')" 211 | file_name_pretty="$(atfile.util.get_file_name_pretty "$(echo "$record" | jq -r '.value')")" 212 | file_size="$(echo "$record" | jq -r '.value.file.size')" 213 | file_size_pretty="$(atfile.util.get_file_size_pretty "$file_size")" 214 | 215 | unset finger_type 216 | header="$file_name_pretty" 217 | 218 | if [[ $(atfile.util.is_null_or_empty "$file_hash_type") == 1 ]] || [[ "$file_hash_type" == "md5" && ${#file_hash} != 32 ]] || [[ "$file_hash_type" == "none" ]]; then 219 | file_hash_pretty="(None)" 220 | fi 221 | 222 | if [[ "$(echo "$record" | jq -r ".value.finger")" != "null" ]]; then 223 | finger_type="$(echo "$record" | jq -r ".value.finger.\"\$type\"" | cut -d "#" -f 2)" 224 | fi 225 | 226 | echo "$header" 227 | atfile.util.print_blob_url_output "$blob_uri" 228 | [[ -n "$cdn_uri" ]] && echo -e " ↳ CDN: $cdn_uri" 229 | echo -e "↳ URI: atfile://$_username/$key" 230 | echo -e "↳ File: $key" 231 | echo -e " ↳ Name: $file_name" 232 | echo -e " ↳ Type: $file_type" 233 | echo -e " ↳ Size: $file_size_pretty" 234 | echo -e " ↳ Date: $(atfile.util.get_date "$file_date" "%Y-%m-%d %H:%M:%S %Z")" 235 | echo -e " ↳ Hash: $file_hash_pretty" 236 | echo -e "↳ Locked: $locked" 237 | echo -e "↳ Encrypted: $encrypted" 238 | if [[ -z "$finger_type" ]]; then 239 | echo -e "↳ Source: (Unknown)" 240 | else 241 | case $finger_type in 242 | "browser") 243 | finger_browser_uas="$(echo "$record" | jq -r ".value.finger.userAgent")" 244 | 245 | [[ -z $finger_browser_uas || $finger_browser_uas == "null" ]] && finger_browser_uas="(Unknown)" 246 | 247 | echo -e "↳ Source: $finger_browser_uas" 248 | ;; 249 | "machine") 250 | finger_machine_app="$(echo "$record" | jq -r ".value.finger.app")" 251 | finger_machine_host="$(echo "$record" | jq -r ".value.finger.host")" 252 | finger_machine_os="$(echo "$record" | jq -r ".value.finger.os")" 253 | 254 | [[ -z $finger_machine_app || $finger_machine_app == "null" ]] && finger_machine_app="(Unknown)" 255 | 256 | echo -e "↳ Source: $finger_machine_app" 257 | [[ -n $finger_machine_host && $finger_machine_host != "null" ]] && echo -e " ↳ Host: $finger_machine_host" 258 | [[ -n $finger_machine_os && $finger_machine_os != "null" ]] && echo -e " ↳ OS: $finger_machine_os" 259 | ;; 260 | *) 261 | echo -e "↳ Source: (Unknown)" 262 | ;; 263 | esac 264 | fi 265 | fi 266 | else 267 | atfile.die "Unable to get '$key'" 268 | fi 269 | } 270 | 271 | function atfile.invoke.get_url() { 272 | key="$1" 273 | unset error 274 | 275 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 276 | record="$(com.atproto.repo.getRecord "$_username" "$_nsid_upload" "$key")" 277 | error="$(atfile.util.get_xrpc_error $? "$record")" 278 | 279 | if [[ -z "$error" ]]; then 280 | blob_url="$(atfile.util.build_blob_uri "$(echo "$record" | jq -r ".uri" | cut -d "/" -f 3)" "$(echo "$record" | jq -r ".value.blob.ref.\"\$link\"")")" 281 | 282 | if [[ $_output_json == 1 ]]; then 283 | echo -e "{\"url\": \"$blob_url\" }" | jq 284 | else 285 | echo "$blob_url" 286 | fi 287 | else 288 | atfile.die.xrpc_error "Unable to get '$key'" "$error" 289 | fi 290 | } 291 | 292 | function atfile.invoke.handle_atfile() { 293 | uri="$1" 294 | handler="$2" 295 | 296 | function atfile.invoke.handle_atfile.is_temp_file_needed() { 297 | handler="${1//.desktop/}" 298 | type="$2" 299 | 300 | handlers=( 301 | "app.drey.EarTag" 302 | "com.github.neithern.g4music" 303 | ) 304 | 305 | if [[ ${handlers[*]} =~ $handler ]]; then 306 | echo 1 307 | elif [[ $type == "text/"* ]]; then 308 | echo 1 309 | else 310 | echo 0 311 | fi 312 | } 313 | 314 | [[ $_output_json == 1 ]] && atfile.die "Command not available as JSON" 315 | 316 | actor="$(echo "$uri" | cut -d "/" -f 3)" 317 | key="$(echo "$uri" | cut -d "/" -f 4)" 318 | 319 | if [[ -n "$actor" && -n "$key" ]]; then 320 | atfile.util.override_actor "$actor" 321 | 322 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 323 | record="$(com.atproto.repo.getRecord "$_username" "$_nsid_upload" "$key")" 324 | error="$(atfile.util.get_xrpc_error $? "$record")" 325 | [[ -n "$error" ]] && atfile.die.gui.xrpc_error "Unable to get '$key'" "$error" 326 | 327 | blob_cid="$(echo "$record" | jq -r ".value.blob.ref.\"\$link\"")" 328 | blob_uri="$(atfile.util.build_blob_uri "$_username" "$blob_cid")" 329 | file_type="$(echo "$record" | jq -r '.value.file.mimeType')" 330 | 331 | if [[ $_os == "linux"* ]] && \ 332 | [ -x "$(command -v xdg-mime)" ] && \ 333 | [ -x "$(command -v xdg-open)" ] && \ 334 | [ -x "$(command -v gtk-launch)" ]; then 335 | 336 | # HACK: Open with browser if $file_type isn't set 337 | [[ -z $file_type ]] && file_type="text/html" 338 | 339 | if [[ -z $handler ]]; then 340 | atfile.say.debug "Querying for handler '$file_type'..." 341 | handler="$(xdg-mime query default "$file_type")" 342 | else 343 | handler="$handler.desktop" 344 | atfile.say.debug "Handler manually set to '$handler'" 345 | fi 346 | 347 | # shellcheck disable=SC2319 348 | # shellcheck disable=SC2181 349 | if [[ -n $handler ]] || [[ $? != 0 ]]; then 350 | atfile.say.debug "Opening '$key' ($file_type) with '${handler//.desktop/}'..." 351 | 352 | # HACK: Some apps don't like http(s)://; we'll need to handle these 353 | if [[ $(atfile.invoke.handle_atfile.is_temp_file_needed "$handler" "$file_type") == 1 ]]; then 354 | atfile.say.debug "Unsupported for streaming" 355 | 356 | download_success=1 357 | # shellcheck disable=SC2154 358 | tmp_path="$_path_blobs_tmp/$blob_cid" 359 | 360 | if ! [[ -f "$tmp_path" ]]; then 361 | atfile.say.debug "Downloading '$blob_cid'..." 362 | atfile.http.download "$blob_uri" "$tmp_path" 363 | [[ $? != 0 ]] && download_success=0 364 | else 365 | atfile.say.debug "Blob '$blob_cid' already exists" 366 | fi 367 | 368 | if [[ $download_success == 1 ]]; then 369 | atfile.say.debug "Launching '$handler'..." 370 | gtk-launch "$handler" "$tmp_path" /dev/null & 371 | else 372 | atfile.die.gui \ 373 | "Unable to download '$key'" 374 | fi 375 | else 376 | atfile.say.debug "Launching '$handler'..." 377 | gtk-launch "$handler" "$blob_uri" /dev/null & 378 | fi 379 | else 380 | atfile.say.debug "No handler for '$file_type'. Launching URI..." 381 | atfile.util.launch_uri "$blob_uri" 382 | fi 383 | else 384 | atfile.say.debug "Relevant tools not installed. Launching URI..." 385 | atfile.util.launch_uri "$blob_uri" 386 | fi 387 | else 388 | atfile.die.gui \ 389 | "Invalid ATFile URI\n↳ Must be 'atfile:///'" \ 390 | "Invalid ATFile URI" 391 | fi 392 | } 393 | 394 | function atfile.invoke.handle_aturi() { 395 | uri="$1" 396 | 397 | [[ $_output_json == 1 ]] && atfile.die "Command not available as JSON" 398 | [[ "$uri" != "at://"* ]] && atfile.die.gui \ 399 | "Invalid AT URI\n↳ Must be 'at://[//]'" \ 400 | "Invalid AT URI" 401 | 402 | atfile.say.debug "Resolving '$uri'..." 403 | app_uri="$(atfile.util.get_app_url_for_at_uri "$uri")" 404 | [[ -z "$app_uri" ]] && atfile.die.gui \ 405 | "Unable to resolve AT URI to App" 406 | 407 | app_proto="$(echo "$app_uri" | cut -d ":" -f 1)" 408 | 409 | atfile.say.debug "Opening '$app_uri' ($app_proto)..." 410 | 411 | if [[ $app_proto == "atfile" ]]; then 412 | atfile.invoke.handle_atfile "$app_uri" 413 | else 414 | atfile.util.launch_uri "$app_uri" 415 | fi 416 | } 417 | 418 | function atfile.invoke.list() { 419 | cursor="$1" 420 | unset error 421 | 422 | atfile.say.debug "Getting records...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username" 423 | records="$(com.atproto.repo.listRecords "$_username" "$_nsid_upload" "$cursor")" 424 | error="$(atfile.util.get_xrpc_error $? "$records")" 425 | 426 | if [[ -z "$error" ]]; then 427 | records="$(echo "$records" | jq -c '.records[]')" 428 | if [[ -z "$records" ]]; then 429 | if [[ -n "$cursor" ]]; then 430 | atfile.die "No more files for '$_username'" 431 | else 432 | atfile.die "No files for '$_username'" 433 | fi 434 | fi 435 | 436 | unset last_key 437 | unset record_count 438 | unset json_output 439 | 440 | if [[ $_output_json == 0 ]]; then 441 | echo -e "Key\t\tFile" 442 | echo -e "---\t\t----" 443 | else 444 | json_output="{\"uploads\":[" 445 | fi 446 | 447 | while IFS=$'\n' read -r c; do 448 | key=$(atfile.util.get_rkey_from_at_uri "$(echo "$c" | jq -r ".uri")") 449 | name="$(echo "$c" | jq -r '.value.file.name')" 450 | type_emoji="$(atfile.util.get_file_type_emoji "$(echo "$c" | jq -r '.value.file.mimeType')")" 451 | last_key="$key" 452 | ((record_count++)) 453 | 454 | if [[ -n $key ]]; then 455 | if [[ $_output_json == 1 ]]; then 456 | json_output+="$c," 457 | else 458 | if [[ $_os == "haiku" ]]; then 459 | # BUG: Haiku Terminal has issues with emojis 460 | echo -e "$key\t$name" 461 | else 462 | echo -e "$key\t$type_emoji $name" 463 | fi 464 | fi 465 | fi 466 | done <<< "$records" 467 | 468 | if [[ $_output_json == 0 ]]; then 469 | atfile.util.print_table_paginate_hint "$last_key" $record_count 470 | else 471 | json_output="${json_output::-1}" 472 | json_output+="]," 473 | json_output+="\"cursor\": \"$last_key\"}" 474 | echo -e "$json_output" | jq 475 | fi 476 | else 477 | atfile.die.xrpc_error "Unable to list files" "$error" 478 | fi 479 | } 480 | 481 | function atfile.invoke.lock() { 482 | key="$1" 483 | locked=$2 484 | unset error 485 | 486 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 487 | upload_record="$(com.atproto.repo.getRecord "$_username" "$_nsid_upload" "$key")" 488 | error=$(atfile.util.get_xrpc_error $? "$upload_record") 489 | 490 | if [[ -z "$error" ]]; then 491 | if [[ $locked == 1 ]]; then 492 | locked=true 493 | else 494 | locked=false 495 | fi 496 | 497 | lock_record="$(blue.zio.atfile.lock $locked)" 498 | 499 | atfile.say.debug "Updating record...\n↳ NSID: $_nsid_lock\n↳ Repo: $_username\n↳ Key: $key" 500 | record="$(com.atproto.repo.putRecord "$_username" "$_nsid_lock" "$key" "$lock_record")" 501 | error=$(atfile.util.get_xrpc_error $? "$record") 502 | fi 503 | 504 | if [[ -z "$error" ]]; then 505 | if [[ $_output_json == 1 ]]; then 506 | echo -e "{ \"locked\": $locked }" | jq 507 | else 508 | if [[ $locked == true ]]; then 509 | echo "Locked: $key" 510 | else 511 | echo "Unlocked: $key" 512 | fi 513 | fi 514 | else 515 | if [[ $locked == true ]]; then 516 | atfile.die "Unable to lock '$key'" "$error" 517 | else 518 | atfile.die "Unable to unlock '$key'" "$error" 519 | fi 520 | fi 521 | } 522 | 523 | function atfile.invoke.print() { 524 | key="$1" 525 | unset error 526 | 527 | atfile.say.debug "Getting record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 528 | record="$(com.atproto.repo.getRecord "$_username" "$_nsid_upload" "$key")" 529 | error="$(atfile.util.get_xrpc_error $? "$record")" 530 | 531 | if [[ -z "$error" ]]; then 532 | blob_uri="$(atfile.util.build_blob_uri "$(echo "$record" | jq -r ".uri" | cut -d "/" -f 3)" "$(echo "$record" | jq -r ".value.blob.ref.\"\$link\"")")" 533 | file_type="$(echo "$record" | jq -r '.value.file.mimeType')" 534 | 535 | curl -H "$(atfile.util.get_uas)" -s -L "$blob_uri" --output - 536 | # shellcheck disable=SC2181 537 | [[ $? != 0 ]] && error="?" 538 | fi 539 | 540 | if [[ -n "$error" ]]; then 541 | atfile.die "Unable to cat '$key'" "$error" 542 | fi 543 | } 544 | 545 | function atfile.invoke.toggle_desktop() { 546 | unset desktop_dir 547 | unset mime_dir 548 | 549 | [[ $_os == "haiku" ]] && atfile.die "Not available on Haiku" 550 | [[ $_os == "macos" ]] && atfile.die "Not available on macOS\nThink you could help? See: https://tangled.sh/@zio.sh/atfile/issues/9" 551 | 552 | uid="$(id -u)" 553 | if [[ $uid == 0 ]]; then 554 | desktop_dir="/usr/local/share/applications" 555 | mime_dir="/usr/local/share/mime" 556 | else 557 | desktop_dir="$HOME/.local/share/applications" 558 | mime_dir="$HOME/.local/share/mime" 559 | fi 560 | 561 | desktop_path="$desktop_dir/atfile-handler.desktop" 562 | mkdir -p "$desktop_dir" 563 | mkdir -p "$mime_dir" 564 | 565 | if [[ -f "$desktop_path" ]]; then 566 | atfile.say "Removing '$desktop_path'..." 567 | rm "$desktop_path" 568 | else 569 | atfile.say "Installing '$desktop_path'..." 570 | 571 | echo "[Desktop Entry] 572 | Name=ATFile (Handler) 573 | Description=Handle atfile:/at: URIs with ATFile 574 | Exec=$_prog_path handle %U 575 | Terminal=false 576 | Type=Application 577 | MimeType=x-scheme-handler/at;x-scheme-handler/atfile; 578 | NoDisplay=true" > "$desktop_path" 579 | fi 580 | 581 | if [ -x "$(command -v xdg-mime)" ] &&\ 582 | [ -x "$(command -v update-mime-database)" ]; then 583 | atfile.say "Updating mime database..." 584 | 585 | update-mime-database "$mime_dir" 586 | xdg-mime default atfile-handler.desktop x-scheme-handler/at 587 | xdg-mime default atfile-handler.desktop x-scheme-handler/atfile 588 | fi 589 | } 590 | 591 | function atfile.invoke.upload() { 592 | file="$1" 593 | recipient="$2" 594 | key="$3" 595 | unset error 596 | 597 | if [[ ! -f "$file" ]]; then 598 | atfile.die "File '$file' does not exist" 599 | else 600 | file="$(atfile.util.get_realpath "$file")" 601 | fi 602 | 603 | if [[ -n $recipient ]]; then 604 | file_crypt="$(dirname "$file")/$(basename "$file").gpg" 605 | 606 | [[ $_output_json == 0 ]] && echo -e "Encrypting '$file_crypt'..." 607 | gpg --yes --quiet --recipient "$recipient" --output "$file_crypt" --encrypt "$file" 608 | 609 | # shellcheck disable=SC2181 610 | if [[ $? == 0 ]]; then 611 | file="$file_crypt" 612 | else 613 | rm -f "$file_crypt" 614 | atfile.die "Unable to encrypt '$(basename "$file")'" 615 | fi 616 | fi 617 | 618 | if [[ -z "$error" ]]; then 619 | unset file_date 620 | unset file_size 621 | unset file_type 622 | 623 | case "$_os" in 624 | "bsd"|"macos") 625 | file_date="$(atfile.util.get_date "$(stat -f '%Sm' -t "%Y-%m-%dT%H:%M:%SZ" "$file")")" 626 | file_size="$(stat -f '%z' "$file")" 627 | file_type="$(file -b --mime-type "$file")" 628 | ;; 629 | "haiku") 630 | haiku_file_attr="$(catattr BEOS:TYPE "$file" 2> /dev/null)" 631 | # shellcheck disable=SC2181 632 | [[ $? == 0 ]] && file_type="$(echo "$haiku_file_attr" | cut -d ":" -f 3 | xargs)" 633 | 634 | file_date="$(atfile.util.get_date "$(stat -c '%y' "$file")")" 635 | file_size="$(stat -c %s "$file")" 636 | ;; 637 | *) 638 | file_date="$(atfile.util.get_date "$(stat -c '%y' "$file")")" 639 | file_size="$(stat -c %s "$file")" 640 | file_type="$(file -b --mime-type "$file")" 641 | ;; 642 | esac 643 | 644 | file_hash="$(atfile.util.get_md5 "$file")" 645 | file_hash_checksum="$(echo "$file_hash" | cut -d "|" -f 1)" 646 | file_hash_type="$(echo "$file_hash" | cut -d "|" -f 2)" 647 | file_name="$(basename "$file")" 648 | 649 | if [[ -n $recipient ]]; then 650 | file_type="application/prs.atfile.gpg-crypt" 651 | elif [[ "$file_type" == "application/"* ]]; then 652 | file_extension="${file_name##*.}" 653 | 654 | case "$file_extension" in 655 | "car") file_type="application/prs.atfile.car" ;; 656 | "dmg"|"smi") file_type="application/x-apple-diskimage" ;; 657 | esac 658 | fi 659 | 660 | file_type_emoji="$(atfile.util.get_file_type_emoji "$file_type")" 661 | 662 | atfile.say.debug "File: $file\n↳ Date: $file_date\n↳ Hash: $file_hash_checksum ($file_hash_type)\n↳ Name: $file_name\n↳ Size: $file_size\n↳ Type: $file_type_emoji $file_type" 663 | 664 | unset file_finger_record 665 | unset file_meta_record 666 | 667 | file_finger_record="$(atfile.util.get_finger_record)" 668 | file_meta_record="$(atfile.util.get_meta_record "$file" "$file_type")" 669 | 670 | atfile.say.debug "Checking filesize..." 671 | file_size_surplus="$(atfile.util.get_file_size_surplus_for_pds "$file_size" "$_server")" 672 | 673 | if [[ $file_size_surplus != 0 ]]; then 674 | die_message="File '$file_name' is too large ($(atfile.util.get_file_size_pretty "$file_size_surplus") over)" 675 | atfile.die "$die_message" 676 | fi 677 | 678 | [[ $_output_json == 0 ]] && echo "Uploading '$file'..." 679 | 680 | blob="$(com.atproto.sync.uploadBlob "$file")" 681 | error="$(atfile.util.get_xrpc_error $? "$blob")" 682 | [[ $error == "?" ]] && error="Blob rejected by PDS (too large?)" 683 | 684 | atfile.say.debug "Uploading blob...\n↳ Ref: $(echo "$blob" | jq -r ".ref.\"\$link\"")" 685 | 686 | if [[ -z "$error" ]]; then 687 | # shellcheck disable=SC2154 688 | file_record="$(blue.zio.atfile.upload "$blob" "$_now" "$file_hash_checksum" "$file_hash_type" "$file_date" "$file_name" "$file_size" "$file_type" "$file_meta_record" "$file_finger_record")" 689 | 690 | if [[ -n "$key" ]]; then 691 | atfile.say.debug "Updating record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username\n↳ Key: $key" 692 | record="$(com.atproto.repo.putRecord "$_username" "$_nsid_upload" "$key" "$file_record")" 693 | error="$(atfile.util.get_xrpc_error $? "$record")" 694 | else 695 | atfile.say.debug "Creating record...\n↳ NSID: $_nsid_upload\n↳ Repo: $_username" 696 | record="$(com.atproto.repo.createRecord "$_username" "$_nsid_upload" "$file_record")" 697 | error="$(atfile.util.get_xrpc_error $? "$record")" 698 | fi 699 | fi 700 | fi 701 | 702 | if [[ -n $recipient ]]; then 703 | rm -f "$file" 704 | fi 705 | 706 | if [[ -z "$error" ]]; then 707 | unset recipient_key 708 | blob_uri="$(atfile.util.build_blob_uri "$(echo "$record" | jq -r ".uri" | cut -d "/" -f 3)" "$(echo "$blob" | jq -r ".ref.\"\$link\"")")" 709 | key="$(atfile.util.get_rkey_from_at_uri "$(echo "$record" | jq -r ".uri")")" 710 | 711 | if [[ -n "$recipient" ]]; then 712 | recipient_key="$(gpg --list-keys "$recipient" | sed -n 2p | xargs)" 713 | fi 714 | 715 | if [[ $_output_json == 1 ]]; then 716 | unset recipient_json 717 | 718 | if [[ -n "$recipient" ]]; then 719 | recipient_json="{ \"id\": \"$recipient\", \"key\": \"$recipient_key\" }" 720 | else 721 | recipient_json="null" 722 | fi 723 | 724 | echo -e "{ \"blob\": \"$blob_uri\", \"key\": \"$key\", \"upload\": $record, \"recipient\": $recipient_json }" | jq 725 | else 726 | echo "---" 727 | if [[ $_os == "haiku" ]]; then 728 | # BUG: Haiku Terminal has issues with emojis 729 | echo "Uploaded: $file_name" 730 | else 731 | echo "Uploaded: $file_type_emoji $file_name" 732 | fi 733 | atfile.util.print_blob_url_output "$blob_uri" 734 | echo -e "↳ Key: $key" 735 | echo -e "↳ URI: atfile://$_username/$key" 736 | if [[ -n "$recipient" ]]; then 737 | echo -e "↳ Recipient: $recipient ($recipient_key)" 738 | fi 739 | fi 740 | else 741 | atfile.die.xrpc_error "Unable to upload '$file'" "$error" 742 | fi 743 | } 744 | -------------------------------------------------------------------------------- /src/shared/util.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function 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 | 11 | function atfile.util.build_out_filename() { 12 | key="$1" 13 | name="$2" 14 | 15 | echo "$_fmt_out_file" | sed -e "s|\[name\]|$name|g" -e "s|\[key\]|$key|g" 16 | } 17 | 18 | function atfile.util.build_query_array() { 19 | key="$1" 20 | values="$2" 21 | 22 | unset query 23 | 24 | if [[ -n "$values" ]]; then 25 | while IFS=$";" read -ra values_array; do 26 | for value in "${values_array[@]}"; do 27 | query+="$key=$value&" 28 | done 29 | done <<< "$values" 30 | fi 31 | 32 | echo "$query" 33 | } 34 | 35 | function atfile.util.check_prog() { 36 | command="$1" 37 | download_hint="$2" 38 | skip_hint="$3" 39 | 40 | atfile.say.debug "Checking program '$1' exists..." 41 | 42 | if ! [ -x "$(command -v "$command")" ]; then 43 | message="'$command' not installed" 44 | 45 | if [[ -n "$download_hint" ]]; then 46 | if [[ "$download_hint" == "http"* ]]; then 47 | message="$message (download: $download_hint)" 48 | else 49 | message="$message (install: \`$download_hint\`)" 50 | fi 51 | fi 52 | 53 | if [[ -n "$skip_hint" ]]; then 54 | message="$message\n↳ This is optional; set ${skip_hint}=1 to ignore" 55 | fi 56 | 57 | atfile.die "$message" 58 | fi 59 | } 60 | 61 | function atfile.util.check_prog_gpg() { 62 | atfile.util.check_prog "gpg" "https://gnupg.org/download" 63 | } 64 | 65 | function atfile.util.check_prog_optional_metadata() { 66 | # shellcheck disable=SC2154 67 | [[ $_disable_ni_exiftool == 0 ]] && atfile.util.check_prog "exiftool" "https://exiftool.org" "${_envvar_prefix}_DISABLE_NI_EXIFTOOL" 68 | # shellcheck disable=SC2154 69 | [[ $_disable_ni_mediainfo == 0 ]] && atfile.util.check_prog "mediainfo" "https://mediaarea.net/en/MediaInfo" "${_envvar_prefix}_DISABLE_NI_MEDIAINFO" 70 | } 71 | 72 | function atfile.util.create_dir() { 73 | dir="$1" 74 | 75 | atfile.say.debug "Creating directory '$dir'..." 76 | 77 | if ! [[ -d $dir ]]; then 78 | mkdir -p "$dir" 79 | # shellcheck disable=SC2181 80 | [[ $? != 0 ]] && atfile.die "Unable to create directory '$dir'" 81 | fi 82 | } 83 | 84 | function atfile.util.fmt_int() { 85 | printf "%'d\n" "$1" 86 | } 87 | 88 | # TODO: Check if record actually exists 89 | function atfile.util.get_app_url_for_at_uri() { 90 | uri="$1" 91 | 92 | actor="$(echo "$uri" | cut -d / -f 3)" 93 | collection="$(echo "$uri" | cut -d / -f 4)" 94 | rkey="$(echo "$uri" | cut -d / -f 5)" 95 | 96 | ignore_url_validation=0 97 | resolved_actor="$(atfile.util.resolve_identity "$actor")" 98 | error="$(atfile.util.get_xrpc_error $? "$resolved_actor")" 99 | [[ -n "$error" ]] && atfile.die.xrpc_error "Unable to resolve '$actor'" "$resolved_actor" 100 | 101 | unset actor_handle 102 | unset actor_pds 103 | unset resolved_url 104 | 105 | # shellcheck disable=SC2181 106 | if [[ $? == 0 ]]; then 107 | actor="$(echo "$resolved_actor" | cut -d "|" -f 1)" 108 | actor_handle="$(echo "$resolved_actor" | cut -d "|" -f 3 | cut -d "/" -f 3)" 109 | actor_pds="$(echo "$resolved_actor" | cut -d "|" -f 2)" 110 | else 111 | unset actor 112 | fi 113 | 114 | [[ -z "$rkey" ]] && rkey="self" 115 | 116 | if [[ -n "$actor" && -n "$collection" && -n "$rkey" ]]; then 117 | case "$collection" in 118 | "app.bsky.actor.profile") resolved_url="$_endpoint_social_app/profile/$actor" ;; 119 | "app.bsky.feed.generator") resolved_url="$_endpoint_social_app/profile/$actor/feed/$rkey" ;; 120 | "app.bsky.graph.list") resolved_url="$_endpoint_social_app/profile/$actor/lists/$rkey" ;; 121 | "app.bsky.graph.starterpack") resolved_url="$_endpoint_social_app/starter-pack/$actor/$rkey" ;; 122 | "app.bsky.feed.post") resolved_url="$_endpoint_social_app/profile/$actor/post/$rkey" ;; 123 | "blue.linkat.board") resolved_url="https://linkat.blue/$actor_handle" ;; 124 | "blue.zio.atfile.upload") ignore_url_validation=1 && resolved_url="atfile://$actor/$rkey" ;; 125 | "chat.bsky.actor.declaration") resolved_url="$_endpoint_social_app/messages/settings" ;; 126 | "com.shinolabs.pinksea.oekaki") resolved_url="https://pinksea.art/$actor/oekaki/$rkey" ;; 127 | "com.whtwnd.blog.entry") resolved_url="https://whtwnd.com/$actor/$rkey" ;; 128 | "events.smokesignal.app.profile") resolved_url="https://smokesignal.events/$actor" ;; 129 | "events.smokesignal.calendar.event") resolved_url="https://smokesignal.events/$actor/$rkey" ;; 130 | "fyi.unravel.frontpage.post") resolved_url="https://frontpage.fyi/post/$actor/$rkey" ;; 131 | "link.pastesphere.snippet") resolved_url="https://pastesphere.link/user/$actor/snippet/$rkey" ;; 132 | "app.bsky.feed.like"| \ 133 | "app.bsky.feed.postgate"| \ 134 | "app.bsky.feed.repost"| \ 135 | "app.bsky.feed.threadgate"| \ 136 | "app.bsky.graph.follow"| \ 137 | "app.bsky.graph.listblock"| \ 138 | "app.bsky.graph.listitem"| \ 139 | "events.smokesignal.calendar.rsvp"| \ 140 | "fyi.unravel.frontpage.comment"| \ 141 | "fyi.unravel.frontpage.vote") 142 | record="$(atfile.xrpc.pds.get "com.atproto.repo.getRecord" "repo=$actor&collection=$collection&rkey=$rkey" "" "$actor_pds")" 143 | 144 | if [[ -z "$(atfile.util.get_xrpc_error $? "$record")" ]]; then 145 | case "$collection" in 146 | "app.bsky.feed.like") 147 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.subject.uri')")" ;; 148 | "app.bsky.feed.postgate") 149 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.post')")" ;; 150 | "app.bsky.feed.repost") 151 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.subject.uri')")" ;; 152 | "app.bsky.feed.threadgate") 153 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.post')")" ;; 154 | "app.bsky.graph.follow") 155 | resolved_url="$_endpoint_social_app/profile/$(echo "$record" | jq -r '.value.subject')" ;; 156 | "app.bsky.graph.listblock") 157 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.subject')")" ;; 158 | "app.bsky.graph.listitem") 159 | resolved_url="$_endpoint_social_app/profile/$(echo "$record" | jq -r '.value.subject')" ;; 160 | "events.smokesignal.calendar.rsvp") 161 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.subject.uri')")" ;; 162 | "fyi.unravel.frontpage.comment") 163 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.post.uri')")/$actor/$rkey" ;; 164 | "fyi.unravel.frontpage.vote") 165 | resolved_url="$(atfile.util.get_app_url_for_at_uri "$(echo "$record" | jq -r '.value.subject.uri')")" ;; 166 | esac 167 | fi 168 | ;; 169 | esac 170 | elif [[ -n "$actor" ]]; then 171 | resolved_url="https://pdsls.dev/at://$actor" 172 | fi 173 | 174 | if [[ -n "$resolved_url" && $ignore_url_validation == 0 ]]; then 175 | if [[ $(atfile.util.is_url_okay "$resolved_url") == 0 ]]; then 176 | unset resolved_url 177 | fi 178 | fi 179 | 180 | echo "$resolved_url" 181 | } 182 | 183 | function atfile.util.get_cache_path() { 184 | # shellcheck disable=SC2154 185 | echo "$_path_cache/$1" 186 | } 187 | 188 | function atfile.util.get_cdn_uri() { 189 | did="$1" 190 | blob_cid="$2" 191 | type="$3" 192 | 193 | cdn_uri="" 194 | 195 | case $type in 196 | "image/jpeg"|"image/png") cdn_uri="https://cdn.bsky.app/img/feed_thumbnail/plain/$did/$blob_cid@jpeg" ;; 197 | esac 198 | 199 | echo "$cdn_uri" 200 | } 201 | 202 | # TODO: Support BusyBox's shit `date` command 203 | # `date -u +"$format" -s "1996-08-11 01:23:34"` 204 | function atfile.util.get_date() { 205 | date="$1" 206 | format="$2" 207 | unset in_format 208 | 209 | [[ -z $format ]] && format="%Y-%m-%dT%H:%M:%SZ" 210 | 211 | 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 212 | if [[ $_os == "bsd" ]]; then 213 | date="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" 214 | in_format="%Y-%m-%d %H:%M:%S" 215 | elif [[ $_os == "linux-musl" || $_os == "solaris" ]]; then 216 | date="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" 217 | fi 218 | fi 219 | 220 | [[ -z $in_format ]] && in_format="$format" 221 | 222 | if [[ -z "$date" ]]; then 223 | if [[ $_os == "linux-musl" || $_os == "solaris" ]]; then 224 | echo "" 225 | else 226 | date -u +"$format" 227 | fi 228 | else 229 | if [[ $_os == "linux-musl" || $_os == "solaris" ]]; then 230 | date -u -d "$date" 231 | elif [[ $_os == "bsd" || $_os == "macos" ]]; then 232 | date -u -j -f "$in_format" "$date" +"$format" 233 | else 234 | date --date "$date" -u +"$format" 235 | fi 236 | fi 237 | } 238 | 239 | function atfile.util.get_date_json() { 240 | date="$1" 241 | parsed="$2" 242 | 243 | if [[ -z "$parsed" ]]; then 244 | if [[ -n "$date" ]]; then 245 | parsed_date="$(atfile.util.get_date "$date" 2> /dev/null)" 246 | # shellcheck disable=SC2181 247 | [[ $? == 0 ]] && parsed="$parsed_date" 248 | fi 249 | fi 250 | 251 | if [[ -n "$parsed" ]]; then 252 | echo "\"$parsed\"" 253 | else 254 | echo "null" 255 | fi 256 | } 257 | 258 | function atfile.util.get_didplc_doc() { 259 | actor="$1" 260 | 261 | function atfile.util.get_didplc_doc.request_doc() { 262 | endpoint="$1" 263 | actor="$2" 264 | 265 | curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$endpoint/$actor" 266 | } 267 | 268 | # shellcheck disable=SC2154 269 | didplc_endpoint="$_endpoint_plc_directory" 270 | didplc_doc="$(atfile.util.get_didplc_doc.request_doc "$didplc_endpoint" "$actor")" 271 | 272 | if [[ "$didplc_doc" != "{"* ]]; then 273 | # shellcheck disable=SC2154 274 | didplc_endpoint="https://plc.directory" 275 | didplc_doc="$(atfile.util.get_didplc_doc.request_doc "$didplc_endpoint" "$actor")" 276 | fi 277 | 278 | echo "$didplc_doc" | jq ". += {\"directory\": \"$didplc_endpoint\"}" 279 | } 280 | 281 | function atfile.util.get_didweb_doc_url() { 282 | actor="$1" 283 | echo "https://${actor//did:web:/}/.well-known/did.json" 284 | } 285 | 286 | function atfile.util.get_envvar() { 287 | envvar="$1" 288 | default="$2" 289 | envvar_from_envfile="$(atfile.util.get_envvar_from_envfile "$envvar")" 290 | envvar_value="" 291 | 292 | if [[ -n "${!envvar}" ]]; then 293 | envvar_value="${!envvar}" 294 | elif [[ -n "$envvar_from_envfile" ]]; then 295 | envvar_value="$envvar_from_envfile" 296 | fi 297 | 298 | if [[ -z "$envvar_value" ]]; then 299 | envvar_value="$default" 300 | fi 301 | 302 | echo "$envvar_value" 303 | } 304 | 305 | function atfile.util.get_envvar_from_envfile() { 306 | variable="$1" 307 | [[ -f $_path_envvar ]] && atfile.util.get_var_from_file "$_path_envvar" "$variable" 308 | } 309 | 310 | function atfile.util.get_exiftool_field() { 311 | file="$1" 312 | tag="$2" 313 | default="$3" 314 | output="" 315 | 316 | exiftool_output="$(eval "exiftool -c \"%+.6f\" -s -T -$tag \"$file\"")" 317 | 318 | if [[ -n "$exiftool_output" ]]; then 319 | if [[ "$exiftool_output" == "-" ]]; then 320 | output="$default" 321 | else 322 | output="$exiftool_output" 323 | fi 324 | else 325 | output="$default" 326 | fi 327 | 328 | echo "$(echo "$output" | sed "s|\"|\\\\\"|g")" 329 | } 330 | 331 | function atfile.util.get_file_name_pretty() { 332 | file_record="$1" 333 | emoji="$(atfile.util.get_file_type_emoji "$(echo "$file_record" | jq -r '.file.mimeType')")" 334 | file_name_no_ext="$(echo "$file_record" | jq -r ".file.name" | cut -d "." -f 1)" 335 | output="$file_name_no_ext" 336 | 337 | meta_type="$(echo "$file_record" | jq -r ".meta.\"\$type\"")" 338 | 339 | if [[ -n "$meta_type" ]]; then 340 | case $meta_type in 341 | "$_nsid_meta#audio") 342 | album="$(echo "$file_record" | jq -r ".meta.tags.album")" 343 | album_artist="$(echo "$file_record" | jq -r ".meta.tags.album_artist")" 344 | date="$(echo "$file_record" | jq -r ".meta.tags.date")" 345 | disc="$(echo "$file_record" | jq -r ".meta.tags.disc.position")" 346 | title="$(echo "$file_record" | jq -r ".meta.tags.title")" 347 | track="$(echo "$file_record" | jq -r ".meta.tags.track.position")" 348 | 349 | [[ $(atfile.util.is_null_or_empty "$album") == 1 ]] && album="(Unknown Album)" 350 | [[ $(atfile.util.is_null_or_empty "$album_artist") == 1 ]] && album_artist="(Unknown Artist)" 351 | [[ $(atfile.util.is_null_or_empty "$disc") == 1 ]] && disc=0 352 | [[ $(atfile.util.is_null_or_empty "$title") == 1 ]] && title="$file_name_no_ext" 353 | [[ $(atfile.util.is_null_or_empty "$track") == 1 ]] && track=0 354 | 355 | output="$title\n $album_artist — $album" 356 | [[ $(atfile.util.is_null_or_empty "$date") == 0 ]] && output+=" ($(atfile.util.get_date "$date" "%Y"))" 357 | [[ $disc != 0 || $track != 0 ]] && output+=" [$disc.$track]" 358 | ;; 359 | "$_nsid_meta#photo") 360 | date="$(echo "$file_record" | jq -r ".meta.date.create")" 361 | lat="$(echo "$file_record" | jq -r ".meta.gps.lat")" 362 | long="$(echo "$file_record" | jq -r ".meta.gps.long")" 363 | title="$(echo "$file_record" | jq -r ".meta.title")" 364 | 365 | [[ -z "$title" ]] && title="$file_name_no_ext" 366 | 367 | output="$title" 368 | 369 | if [[ $(atfile.util.is_null_or_empty "$lat") == 0 && $(atfile.util.is_null_or_empty "$long") == 0 ]]; then 370 | output+="\n $long $lat" 371 | 372 | if [[ $(atfile.util.is_null_or_empty "$date") == 0 ]]; then 373 | output+=" — $(atfile.util.get_date "$date")" 374 | fi 375 | fi 376 | ;; 377 | "$_nsid_meta#video") 378 | title="$(echo "$file_record" | jq -r ".meta.tags.title")" 379 | 380 | [[ $(atfile.util.is_null_or_empty "$title") == 1 ]] && title="$file_name_no_ext" 381 | 382 | output="$title" 383 | ;; 384 | esac 385 | fi 386 | 387 | # BUG: Haiku Terminal has issues with emojis 388 | if [[ $_os != "haiku" ]]; then 389 | output="$emoji $output" 390 | fi 391 | 392 | output_last_line="$(echo -e "$output" | tail -n1)" 393 | output_last_line_length="${#output_last_line}" 394 | 395 | echo -e "$output" 396 | echo -e "$(atfile.util.repeat_char "-" "$output_last_line_length")" 397 | } 398 | 399 | function atfile.util.get_file_size_pretty() { 400 | size="$1" 401 | suffix="" 402 | 403 | if (( size >= 1048576 )); then 404 | size=$(( size / 1048576 )) 405 | suffix="MiB" 406 | elif (( size >= 1024 )); then 407 | size=$(( size / 1024 )) 408 | suffix="KiB" 409 | else 410 | suffix="B" 411 | fi 412 | 413 | echo "$size $suffix" 414 | } 415 | 416 | # NOTE: There is currently no API for getting the filesize limit on the server 417 | function atfile.util.get_file_size_surplus_for_pds() { 418 | size="$1" 419 | pds="$2" 420 | 421 | unset max_filesize 422 | 423 | case $pds in 424 | *".host.bsky.network") max_filesize=1073741824 ;; 425 | esac 426 | 427 | if [[ -z $max_filesize ]] || [[ $max_filesize == 0 ]] || (( size < max_filesize )); then 428 | echo 0 429 | else 430 | echo $(( size - max_filesize )) 431 | fi 432 | } 433 | 434 | function atfile.util.get_file_type_emoji() { 435 | mime_type="$1" 436 | short_type="$(echo "$mime_type" | cut -d "/" -f 1)" 437 | desc_type="$(echo "$mime_type" | cut -d "/" -f 2)" 438 | 439 | case $short_type in 440 | "application") 441 | case "$desc_type" in 442 | # Apps (Desktop) 443 | "vnd.debian.binary-package"| \ 444 | "vnd.microsoft.portable-executable"| \ 445 | "x-executable"| \ 446 | "x-rpm") 447 | echo "💻" ;; 448 | # Apps (Mobile) 449 | "vnd.android.package-archive"| \ 450 | "x-ios-app") 451 | echo "📱" ;; 452 | # Archives 453 | "prs.atfile.car"| \ 454 | "gzip"|"x-7z-compressed"|"x-apple-diskimage"|"x-bzip2"|"x-stuffit"|"x-xz"|"zip") 455 | echo "📦" ;; 456 | # Disk Images 457 | "x-iso9660-image") 458 | echo "💿" ;; 459 | # Encrypted 460 | "prs.atfile.gpg-crypt") 461 | echo "🔑" ;; 462 | # Rich Text 463 | "pdf"| \ 464 | "vnd.oasis.opendocument.text") 465 | echo "📄" ;; 466 | *) echo "⚙️ " ;; 467 | esac 468 | ;; 469 | "audio") echo "🎵" ;; 470 | "font") echo "✏️" ;; 471 | "image") echo "🖼️ " ;; 472 | "inode") echo "🔌" ;; 473 | "text") 474 | case "$mime_type" in 475 | "text/x-shellscript") echo "⚙️ " ;; 476 | *) echo "📄" ;; 477 | esac 478 | ;; 479 | "video") echo "📼" ;; 480 | *) echo "❓" ;; 481 | esac 482 | } 483 | 484 | function atfile.util.get_int_suffix() { 485 | int="$1" 486 | singular="$2" 487 | plural="$3" 488 | 489 | [[ $int == 1 ]] && echo -e "$singular" || echo -e "$plural" 490 | } 491 | 492 | function atfile.util.get_finger_record() { 493 | fingerprint_override="$1" 494 | unset enable_fingerprint_original 495 | 496 | if [[ $fingerprint_override ]]; then 497 | enable_fingerprint_original="$_enable_fingerprint" 498 | _enable_fingerprint="$fingerprint_override" 499 | fi 500 | 501 | echo -e "$(blue.zio.atfile.finger__machine)" 502 | 503 | if [[ -n $enable_fingerprint_original ]]; then 504 | _enable_fingerprint="$enable_fingerprint_original" 505 | fi 506 | } 507 | 508 | function atfile.util.get_line() { 509 | input="$1" 510 | index=$(( $2 + 1 )) 511 | 512 | echo -e "$input" | sed -n "$(( index ))"p 513 | } 514 | 515 | function atfile.util.get_mediainfo_field() { 516 | file="$1" 517 | category="$2" 518 | field="$3" 519 | default="$4" 520 | output="" 521 | 522 | mediainfo_output="$(mediainfo --Inform="$category;%$field%\n" "$file")" 523 | 524 | if [[ -n "$mediainfo_output" ]]; then 525 | if [[ "$mediainfo_output" == "None" ]]; then 526 | output="$default" 527 | else 528 | output="$mediainfo_output" 529 | fi 530 | else 531 | output="$default" 532 | fi 533 | 534 | echo "$(echo "$output" | sed "s|\"|\\\\\"|g")" 535 | } 536 | 537 | function atfile.util.get_mediainfo_audio_json() { 538 | file="$1" 539 | 540 | bitRates=$(atfile.util.get_mediainfo_field "$file" "Audio" "BitRate" 0) 541 | bitRate_modes=$(atfile.util.get_mediainfo_field "$file" "Audio" "BitRate_Mode" "") 542 | channelss=$(atfile.util.get_mediainfo_field "$file" "Audio" "Channels" 0) 543 | compressions="$(atfile.util.get_mediainfo_field "$file" "Audio" "Compression_Mode" "")" 544 | durations=$(atfile.util.get_mediainfo_field "$file" "Audio" "Duration" 0) 545 | formats="$(atfile.util.get_mediainfo_field "$file" "Audio" "Format" "")" 546 | format_ids="$(atfile.util.get_mediainfo_field "$file" "Audio" "CodecID" "")" 547 | format_profiles="$(atfile.util.get_mediainfo_field "$file" "Audio" "Format_Profile" "")" 548 | samplings=$(atfile.util.get_mediainfo_field "$file" "Audio" "SamplingRate" 0) 549 | titles="$(atfile.util.get_mediainfo_field "$file" "Audio" "Title" "")" 550 | 551 | lines="$(echo "$bitRates" | wc -l)" 552 | output="" 553 | 554 | for (( i = 0 ; i < lines ; i++ )); do 555 | lossy=true 556 | 557 | [[ $(atfile.util.get_line "$compressions" $i) == "Lossless" ]] && lossy=false 558 | 559 | output+="{ 560 | \"bitRate\": $(atfile.util.get_line "$bitRates" $i), 561 | \"channels\": $(atfile.util.get_line "$channelss" $i), 562 | \"duration\": $(atfile.util.get_line "$durations" $i), 563 | \"format\": { 564 | \"id\": \"$(atfile.util.get_line "$format_ids" $i)\", 565 | \"name\": \"$(atfile.util.get_line "$formats" $i)\", 566 | \"profile\": \"$(atfile.util.get_line "$format_profiles" $i)\" 567 | }, 568 | \"mode\": \"$(atfile.util.get_line "$bitRate_modes" $i)\", 569 | \"lossy\": $lossy, 570 | \"sampling\": $(atfile.util.get_line "$samplings" $i), 571 | \"title\": \"$(atfile.util.get_line "$titles" $i)\" 572 | }," 573 | done 574 | 575 | echo "${output::-1}" 576 | } 577 | 578 | function atfile.util.get_mediainfo_video_json() { 579 | file="$1" 580 | 581 | bitRates=$(atfile.util.get_mediainfo_field "$file" "Video" "BitRate" 0) 582 | dim_height=$(atfile.util.get_mediainfo_field "$file" "Video" "Height" 0) 583 | dim_width=$(atfile.util.get_mediainfo_field "$file" "Video" "Width" 0) 584 | durations=$(atfile.util.get_mediainfo_field "$file" "Video" "Duration" 0) 585 | formats="$(atfile.util.get_mediainfo_field "$file" "Video" "Format" "")" 586 | format_ids="$(atfile.util.get_mediainfo_field "$file" "Video" "CodecID" "")" 587 | format_profiles="$(atfile.util.get_mediainfo_field "$file" "Video" "Format_Profile" "")" 588 | frameRates="$(atfile.util.get_mediainfo_field "$file" "Video" "FrameRate" "")" 589 | frameRate_modes="$(atfile.util.get_mediainfo_field "$file" "Video" "FrameRate_Mode" "")" 590 | titles="$(atfile.util.get_mediainfo_field "$file" "Video" "Title" "")" 591 | 592 | lines="$(echo "$bitRates" | wc -l)" 593 | output="" 594 | 595 | for ((i = 0 ; i < lines ; i++ )); do 596 | output+="{ 597 | \"bitRate\": $(atfile.util.get_line "$bitRates" $i), 598 | \"dimensions\": { 599 | \"height\": $dim_height, 600 | \"width\": $dim_width 601 | }, 602 | \"duration\": $(atfile.util.get_line "$durations" $i), 603 | \"format\": { 604 | \"id\": \"$(atfile.util.get_line "$format_ids" $i)\", 605 | \"name\": \"$(atfile.util.get_line "$formats" $i)\", 606 | \"profile\": \"$(atfile.util.get_line "$format_profiles" $i)\" 607 | }, 608 | \"frameRate\": $(atfile.util.get_line "$frameRates" $i), 609 | \"mode\": \"$(atfile.util.get_line "$frameRate_modes" $i)\", 610 | \"title\": \"$(atfile.util.get_line "$titles" $i)\" 611 | }," 612 | done 613 | 614 | echo "${output::-1}" 615 | } 616 | 617 | function atfile.util.get_meta_record() { 618 | file="$1" 619 | type="$2" 620 | 621 | case "$type" in 622 | "audio/"*) blue.zio.atfile.meta__audio "$1" ;; 623 | "image/"*) blue.zio.atfile.meta__photo "$1" ;; 624 | "video/"*) blue.zio.atfile.meta__video "$1" ;; 625 | *) blue.zio.atfile.meta__unknown "" "$type" ;; 626 | esac 627 | } 628 | 629 | function atfile.util.get_md5() { 630 | file="$1" 631 | 632 | unset checksum 633 | type="none" 634 | 635 | if [ -x "$(command -v md5sum)" ]; then 636 | hash="$(md5sum "$file" | cut -f 1 -d " ")" 637 | if [[ ${#hash} == 32 ]]; then 638 | checksum="$hash" 639 | type="md5" 640 | fi 641 | fi 642 | 643 | echo "$checksum|$type" 644 | } 645 | 646 | function atfile.util.get_os() { 647 | os="${OSTYPE,,}" 648 | 649 | [[ -n $_force_os ]] && os="force-${_force_os,,}" 650 | 651 | # shellcheck disable=SC2221 652 | # shellcheck disable=SC2222 653 | case $os in 654 | # BSD 655 | "freebsd"*|"netbsd"*|"openbsd"*|*"bsd"|"force-bsd") echo "bsd" ;; 656 | # Haiku 657 | "haiku"|"force-haiku") echo "haiku" ;; 658 | # Linux 659 | "linux-gnu"|"force-linux") echo "linux" ;; 660 | "cygwin"|"msys"|"force-linux-mingw") echo "linux-mingw" ;; 661 | "linux-musl"|"force-linux-musl") echo "linux-musl" ;; 662 | "linux-android"|"force-linux-termux") echo "linux-termux" ;; 663 | # macOS 664 | "darwin"*|"force-macos") echo "macos" ;; 665 | # SerenityOS 666 | "serenity"*|"force-serenity") echo "serenity" ;; 667 | # Solaris 668 | "solaris"*|"force-solaris") echo "solaris" ;; 669 | # Unknown 670 | *) echo "unknown-${os//force-/}" ;; 671 | esac 672 | } 673 | 674 | function atfile.util.get_pds_pretty() { 675 | pds="$1" 676 | 677 | pds_host="$(atfile.util.get_uri_segment "$pds" host)" 678 | unset pds_name 679 | unset pds_emoji 680 | 681 | case "$pds_host" in 682 | *".host.bsky.network") 683 | bsky_host="$(echo "$pds_host" | cut -d "." -f 1)" 684 | bsky_region="$(echo "$pds_host" | cut -d "." -f 2)" 685 | 686 | pds_name="${bsky_host^} ($(atfile.util.get_region_pretty "$bsky_region"))" 687 | pds_emoji="🍄" 688 | ;; 689 | "at.app.wafrn.net") pds_name="Wafrn"; pds_emoji="🌸" ;; 690 | "atproto.brid.gy") pds_name="Bridgy Fed"; pds_emoji="🔀" ;; 691 | "blacksky.app") pds_name="Blacksky"; pds_emoji="⬛" ;; 692 | "pds.sprk.so") pds_name="Spark"; pds_emoji="✨" ;; 693 | "tngl.sh") pds_name="Tangled"; pds_emoji="🪢" ;; 694 | *) 695 | pds_oauth_url="$pds/oauth/authorize" 696 | pds_oauth_page="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$pds_oauth_url" | tr -d '\n')" 697 | pds_customization_data="$(echo "$pds_oauth_page" | sed -n 's/.*window\["__customizationData"\]=JSON.parse("\(.*\)");.*/\1/p' | sed 's/\\"/"/g; s/\\\\/\\/g' | sed 's/");window\[".*$//')" 698 | 699 | if [[ $pds_customization_data == "{"* ]]; then 700 | pds_name="$(echo "$pds_customization_data" | jq -r '.name')" 701 | pds_emoji="🟦" 702 | else 703 | pds_name="$pds_host" 704 | fi 705 | ;; 706 | esac 707 | 708 | # BUG: Haiku Terminal has issues with emojis 709 | if [[ -n "$pds_emoji" ]] && [[ $_os != "haiku" ]]; then 710 | echo "$pds_emoji $pds_name" 711 | else 712 | echo "$pds_name" 713 | fi 714 | } 715 | 716 | function atfile.util.get_random() { 717 | amount="$1" 718 | [[ -z "$amount" ]] && amount="6" 719 | echo "$(tr -dc A-Za-z0-9 = _max_list )); then 1055 | first_line="List is limited to $_max_list results. To print more results," 1056 | first_line_length=$(( ${#first_line} + 3 )) 1057 | # shellcheck disable=SC2154 1058 | echo -e "$(atfile.util.repeat_char "-" $first_line_length)\nℹ️ $first_line\n run \`$_prog $_command $cursor\`" 1059 | fi 1060 | } 1061 | 1062 | function atfile.util.repeat_char() { 1063 | char="$1" 1064 | amount="$2" 1065 | 1066 | if [ -x "$(command -v seq)" ]; then 1067 | printf "%0.s$char" $(seq 1 "$amount") 1068 | else 1069 | echo "$char" 1070 | fi 1071 | } 1072 | 1073 | function atfile.util.resolve_identity() { 1074 | actor="$1" 1075 | 1076 | if [[ "$actor" != "did:"* ]]; then 1077 | # shellcheck disable=SC2154 1078 | resolved_handle="$(atfile.xrpc.bsky.get "com.atproto.identity.resolveHandle" "handle=$actor")" 1079 | error="$(atfile.util.get_xrpc_error $? "$resolved_handle")" 1080 | 1081 | if [[ -z "$error" ]]; then 1082 | actor="$(echo "$resolved_handle" | jq -r ".did")" 1083 | fi 1084 | fi 1085 | 1086 | if [[ "$actor" == "did:"* ]]; then 1087 | unset did_doc 1088 | 1089 | case "$actor" in 1090 | "did:plc:"*) did_doc="$(atfile.util.get_didplc_doc "$actor")" ;; 1091 | "did:web:"*) did_doc="$(curl -H "User-Agent: $(atfile.util.get_uas)" -s -L -X GET "$(atfile.util.get_didweb_doc_url "$actor")")" ;; 1092 | *) echo "Unknown DID type 'did:$(echo "$actor" | cut -d ":" -f 2)'"; exit 255;; 1093 | esac 1094 | 1095 | if [[ -n "$did_doc" ]]; then 1096 | did="$(echo "$did_doc" | jq -r ".id")" 1097 | 1098 | if [[ $(atfile.util.is_null_or_empty "$did") == 1 ]]; then 1099 | echo "$error" 1100 | exit 255 1101 | fi 1102 | 1103 | unset aliases 1104 | unset handle 1105 | didplc_dir="$(echo "$did_doc" | jq -r ".directory")" 1106 | pds="$(echo "$did_doc" | jq -r '.service[] | select(.id == "#atproto_pds") | .serviceEndpoint')" 1107 | 1108 | while IFS=$'\n' read -r a; do 1109 | aliases+="$a;" 1110 | 1111 | if [[ -z $handle && "$a" == "at://"* && "$a" != "at://did:"* ]]; then 1112 | handle="$a" 1113 | fi 1114 | done <<< "$(echo "$did_doc" | jq -r '.alsoKnownAs[]')" 1115 | 1116 | [[ $didplc_dir == "null" ]] && unset didplc_dir 1117 | [[ -z "$handle" ]] && handle="invalid.handle" 1118 | 1119 | echo "$did|$pds|$handle|$didplc_dir|$aliases" 1120 | fi 1121 | else 1122 | echo "$error" 1123 | exit 255 1124 | fi 1125 | } 1126 | 1127 | function atfile.util.write_cache() { 1128 | file="$1" 1129 | file_path="$_path_cache/$1" 1130 | content="$2" 1131 | 1132 | atfile.util.get_cache "$file" 1133 | 1134 | echo -e "$content" > "$file_path" 1135 | # shellcheck disable=SC2320 1136 | # shellcheck disable=SC2181 1137 | [[ $? != 0 ]] && atfile.die "Unable to write to cache file ($file)" 1138 | } 1139 | --------------------------------------------------------------------------------