atproto library for bash scripts
atproto bash-atproto

Compare changes

Choose any two refs to compare.

+17 -13
README.md
··· 1 1 # bash-atproto 2 2 3 - bash-atproto is a bare-bones atproto client/library that I wrote for [imasimgbot](https://github.com/Engielolz/imasimgbot) and the now-defunct [765coverbot](https://tangled.sh/@did:plc:s2cyuhd7je7eegffpnurnpud/765coverbot). It is a Bash script that uses cURL to authenticate, create records and upload blobs. 3 + bash-atproto is a bare-bones atproto client/library that I wrote for [imasimgbot](https://tangled.org/did:plc:s2cyuhd7je7eegffpnurnpud/imasimgbot) and the now-defunct [765coverbot](https://tangled.org/did:plc:s2cyuhd7je7eegffpnurnpud/765coverbot). It is a Bash script that uses cURL to authenticate, create records and upload blobs. 4 4 5 5 ## Supported functions 6 6 7 7 Functions built into `bash-atproto.sh` 8 8 9 - * Resolving a handle to did:plc/did:web - `bap_didInit` 10 - * Resolving an account's PDS from the DID - `bap_findPDS` 9 + * Fetch a record from URI - `bap_getRecord` 10 + * Resolving a handle to did:plc/did:web - `bap_getDID` 11 + * Resolving the DID doc for a DID - `bap_resolveDID` 12 + * Resolving a handle from a DID - `bap_resolveHandle` 13 + * Resolving an account's PDS from the DID - `bap_resolvePDS` 11 14 * Creating and closing sessions on the PDS - `bap_getKeys` and `bap_closeSession` 12 15 * Saving and loading a secrets file (contains your access and refresh tokens) - `bap_saveSecrets` and `bap_loadSecrets` 13 16 * Extracting account and token information from the access token - (automatic) 14 17 * Refreshing tokens - `bap_refreshKeys` 18 + * Minting service auth tokens - `bap_getServiceAuth` 15 19 * Prepare an image, stripping EXIF data and (if needed) resizing it for a set file size and dimensions - `bap_prepareImage` 16 20 * Uploading blobs - `bap_uploadBlobToPDS` 17 21 * Working with records: ··· 34 38 * Prepare an image for upload - `bapBsky_prepareImage` 35 39 * Add an image - `bapBsky_cyorAddImage` 36 40 * Add a video - `bapBsky_cyorAddVideo` 41 + * Add an embed - `bapBsky_cyorAddExternalEmbed` 37 42 * Set language - `bapBsky_cyorAddLangs` 38 43 * Add tags - `bapBsky_cyorAddTags` 39 44 * Add self-labels - `bapBsky_cyorAddSelfLabels` ··· 55 60 56 61 ## Dependencies 57 62 58 - bash-atproto requires cURL 7.76 or later and jq 1.7 or later. Posting images and video additionally requires `imagemagick`, `exiftool`, `uuidgen` and `file`. 63 + bash-atproto requires `curl` 7.76 or later, `jq` 1.7 or later and `dig`. Posting images and video additionally requires `imagemagick`, `exiftool`, `uuidgen` and `file`. 59 64 60 65 ## Basic usage 61 66 62 - bash-atproto is loaded with `source bash-atproto.sh`. From there, most operations will require you to sign-in to an atproto account, which can be done in three functions: 67 + bash-atproto is loaded with `source bash-atproto.sh`. From there, most operations will require you to sign-in to an atproto account, which can be done in one function: 63 68 64 - 1. `bap_didInit <did or handle>` which will resolve your handle to a DID 65 - 2. `bap_findPDS $savedDID` which will retrieve the account's PDS for use 66 - 3. `bap_getKeys $savedDID <password>` to log in. The access and refresh tokens are saved in memory and can be written to disk with `bap_saveSecrets <file>`. It is recommended you use an App Password to log in rather than a normal password. 69 + * `bap_getKeys <did or handle> <password>` 70 + 71 + The access and refresh tokens are saved in memory and can be written to disk with `bap_saveSecrets <file>`. It is recommended you use an App Password to log in rather than a normal password. 67 72 68 73 You may need to load additional scripts to handle other tasks. For example, to post to Bluesky, you will need to `source bap-bsky.sh`. 69 74 ··· 74 79 bash-atproto is mainly meant for use as a component for bash scripts that want to use the AT Protocol, but it by itself can be used as a simple atproto client for the command line. The following commands will log in to your PDS, create a Bluesky post and then log out: 75 80 76 81 1. `source bash-atproto.sh` 77 - 2. `bap_didInit <your did or handle>` 78 - 3. `bap_findPDS $savedDID` 79 - 4. `bap_getKeys $savedDID <app password>` 80 - 5. `bap_postToBluesky "Hello, World!" en` 81 - 6. `bap_closeSession` 82 + 2. `source bap-bsky.sh` 83 + 3. `bap_getKeys <your did or handle> <app password>` 84 + 4. `bapBsky_createPost "Hello, World!" en` 85 + 5. `bap_closeSession` 82 86 83 87 ## License 84 88
+188 -140
bap-bsky.sh
··· 2 2 # SPDX-License-Identifier: MIT 3 3 # bap-bsky.sh: the bash-atproto functions pertaining to Bluesky Social. 4 4 if [ -z "$bap_internalVersion" ]; then >&2 echo "bash-atproto not loaded?"; return 127; fi 5 - if [ "$bap_internalVersion" != "3" ] || ! [ "$bap_internalMinorVer" -ge "3" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 5 + if [ "$bap_internalVersion" != "4" ] || ! [ "$bap_internalMinorVer" -ge "-1" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 6 6 7 - bapBsky_internalVersion=1 8 - bapBsky_internalMinorVer=4 9 - bapBsky_allowLegacyPre2=1 7 + bapBsky_internalVersion=2 8 + bapBsky_internalMinorVer=0 9 + bapBsky_bskyAppViewDID=did:web:api.bsky.app#bsky_appview 10 + bapBsky_lumiURL=video.bsky.app 10 11 11 12 function bapBskyErr () { 12 13 >&2 echo "bap-bsky: $*" ··· 17 18 echo "bap-bsky: $*" 18 19 } 19 20 21 + function bapBskyErrVerb () { 22 + if [ ! "$bap_verbosity" -ge 2 ]; then return 0; fi 23 + >&2 echo "bap-bsky: $*" 24 + } 25 + 20 26 function bapBsky_cyorInit () { 21 27 bap_cyorRecord="{}" 22 28 bapCYOR_str \$type app.bsky.feed.post ··· 24 30 } 25 31 26 32 function bapBskyInternal_convertToRecordWithMedia () { 27 - cyorTemp=$(echo "$bap_cyorRecord $(echo $bap_cyorRecord | jq -r .embed | echo {\"embed\": {\"$1\": $(</dev/stdin)}})" | jq -s add) || return 1 33 + cyorTemp=$(echo "$bap_cyorRecord $(echo "$bap_cyorRecord" | jq -r .embed | echo "{\"embed\": {\"$1\": $(</dev/stdin)}}")" | jq -s add) || return 1 28 34 bap_cyorRecord=$cyorTemp 29 35 bapCYOR_str \$type "app.bsky.embed.recordWithMedia" .embed 30 36 bapCYOR_str \$type "app.bsky.embed.record" .embed.record ··· 32 38 33 39 function bapBsky_cyorAddImage () { 34 40 if [ -z "$6" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 35 - if [ "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" = "app.bsky.embed.record" ]; then bapBskyInternal_convertToRecordWithMedia record; fi 36 - if [ "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" = "app.bsky.embed.recordWithMedia" ]; then local l=.media; fi 41 + if [ "$(echo "$bap_cyorRecord" | jq -r ".embed.[\"\$type\"]")" = "app.bsky.embed.record" ]; then bapBskyInternal_convertToRecordWithMedia record; fi 42 + if [ "$(echo "$bap_cyorRecord" | jq -r ".embed.[\"\$type\"]")" = "app.bsky.embed.recordWithMedia" ]; then local l=.media; fi 37 43 # it's easy but just LOOK at all those commands 38 - bapCYOR_str \$type app.bsky.embed.images .embed$l 39 - if [ -n "$7" ]; then bapCYOR_str alt "$7" .embed$l.images.[$1]; fi 40 - bapCYOR_str \$type blob .embed$l.images.[$1].image 41 - bapCYOR_str \$link $2 .embed$l.images.[$1].image.ref 42 - bapCYOR_str mimeType "$3" .embed$l.images.[$1].image 43 - bapCYOR_add size $4 .embed$l.images.[$1].image 44 - bapCYOR_add width $5 .embed$l.images.[$1].aspectRatio 45 - bapCYOR_add height $6 .embed$l.images.[$1].aspectRatio 44 + bapCYOR_str \$type app.bsky.embed.images ".embed$l" 45 + if [ -n "$7" ]; then bapCYOR_str alt "$7" ".embed$l.images.[$1]"; fi 46 + bapCYOR_str \$type blob ".embed$l.images.[$1].image" 47 + bapCYOR_str \$link "$2" ".embed$l.images.[$1].image.ref" 48 + bapCYOR_str mimeType "$3" ".embed$l.images.[$1].image" 49 + bapCYOR_add size "$4" ".embed$l.images.[$1].image" 50 + bapCYOR_add width "$5" ".embed$l.images.[$1].aspectRatio" 51 + bapCYOR_add height "$6" ".embed$l.images.[$1].aspectRatio" 46 52 } 47 53 48 54 function bapBsky_cyorAddVideo () { ··· 53 59 # 4 - height 54 60 # 5 - alt text 55 61 if [ -z "$4" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 56 - if [ "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" = "app.bsky.embed.record" ]; then bapBskyInternal_convertToRecordWithMedia record; fi 57 - if [ "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" = "app.bsky.embed.recordWithMedia" ]; then local l=.media; fi 58 - bapCYOR_str alt "$5" .embed$l 59 - bapCYOR_str \$type app.bsky.embed.video .embed$l 60 - bapCYOR_str \$type blob .embed$l.video 61 - bapCYOR_str \$link "$1" .embed$l.video.ref 62 - bapCYOR_str mimeType "video/mp4" .embed$l.video 63 - bapCYOR_add size $2 .embed$l.video 64 - bapCYOR_add width $3 .embed$l.aspectRatio 65 - bapCYOR_add height $4 .embed$l.aspectRatio 62 + if [ "$(echo "$bap_cyorRecord" | jq -r ".embed.[\"\$type\"]")" = "app.bsky.embed.record" ]; then bapBskyInternal_convertToRecordWithMedia record; fi 63 + if [ "$(echo "$bap_cyorRecord" | jq -r ".embed.[\"\$type\"]")" = "app.bsky.embed.recordWithMedia" ]; then local l=.media; fi 64 + bapCYOR_str alt "$5" ".embed$l" 65 + bapCYOR_str \$type app.bsky.embed.video ".embed$l" 66 + bapCYOR_str \$type blob ".embed$l.video" 67 + bapCYOR_str \$link "$1" ".embed$l.video.ref" 68 + bapCYOR_str mimeType "video/mp4" ".embed$l.video" 69 + bapCYOR_add size "$2" ".embed$l.video" 70 + bapCYOR_add width "$3" ".embed$l.aspectRatio" 71 + bapCYOR_add height "$4" ".embed$l.aspectRatio" 66 72 } 67 73 68 - # (porn, sexual, nudity), graphic-media 69 - function bapBsky_cyorAddLabel () { 70 - if [ "$bapBsky_allowLegacyPre2" != "1" ]; then >&2 echo "${FUNCNAME[0]}: command not found"; return 127; fi 71 - bapBskyErr "warn: function ${FUNCNAME[0]} is deprecated. use bapBsky_cyorAddSelfLabels instead" 72 - if [ -z "$2" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 73 - bapCYOR_str \$type com.atproto.label.defs#selfLabels .labels 74 - # can you use multiple labels like this? 75 - bapCYOR_str val $2 .labels.values.[$1] 76 - } 77 - 78 - function bapBsky_cyorGetReplyRoot () { 74 + function bapBskyInternal_cyorGetReplyRoot () { 79 75 if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 80 - bapBsky_temp=$(bap_getRecord $1) || return $? 81 - if echo $bapBsky_temp | jq -re '.value.reply.root.uri' > /dev/null; then 76 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 77 + if echo "$bapBsky_temp" | jq -re '.record.reply.root.uri' > /dev/null; then 82 78 # copy values 83 - bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.value.reply.root.uri') .reply.root 84 - bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.value.reply.root.cid') .reply.root 79 + bapCYOR_str uri "$(echo "$bapBsky_temp" | jq -re '.record.reply.root.uri')" .reply.root 80 + bapCYOR_str cid "$(echo "$bapBsky_temp" | jq -re '.record.reply.root.cid')" .reply.root 81 + elif [ -z "$bapBsky_temp" ]; then 82 + bapBskyErr "failed to resolve post" 83 + return 1 85 84 else 86 85 # we just wasted time and bandwidth! 87 - bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .reply.root 88 - bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.cid') .reply.root 86 + bapCYOR_str uri "$(echo "$bapBsky_temp" | jq -re '.uri')" .reply.root 87 + bapCYOR_str cid "$(echo "$bapBsky_temp" | jq -re '.cid')" .reply.root 89 88 fi 90 89 return 0 91 90 } 92 91 93 92 function bapBsky_cyorAddReply () { 94 93 if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 95 - bapBsky_temp=$(bap_getRecord $1) || return $? 96 - bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .reply.parent 97 - bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.cid') .reply.parent 98 - if ! echo $bap_cyorRecord | jq -re '.reply.root' > /dev/null; then bapBsky_cyorGetReplyRoot $1 || return $?; fi 94 + local bapBsky_temp bapBsky_tempPost=$bap_cyorRecord 95 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 96 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.replyDisabled)" = "true" ]; then bapBskyErr "specified post disallows replies"; return 2; fi 97 + bapCYOR_str uri "$(echo "$bapBsky_temp" | jq -re '.uri')" .reply.parent 98 + bapCYOR_str cid "$(echo "$bapBsky_temp" | jq -re '.cid')" .reply.parent 99 + bapBskyInternal_cyorGetReplyRoot "$1" || { bap_cyorRecord=$bapBsky_tempPost; return 1; } 99 100 return 0 100 101 } 101 102 ··· 127 128 return 0 128 129 } 129 130 131 + # (porn, sexual, nudity), graphic-media 130 132 function bapBsky_cyorAddSelfLabels () { 131 133 if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 132 134 bapCYOR_str \$type com.atproto.label.defs#selfLabels .labels ··· 143 145 function bapBsky_cyorAddQuote () { 144 146 if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 145 147 local bapBsky_temp cyorTemp l 146 - bapBsky_temp=$(bap_getRecord $1) || return $? 147 - case "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" in 148 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 149 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.embeddingDisabled)" = "true" ]; then bapBskyErr "specified post disallows quotes"; return 2; fi 150 + case "$(echo "$bap_cyorRecord" | jq -r ".record.embed.[\"\$type\"]")" in 148 151 "null") 149 152 bapCYOR_str \$type "app.bsky.embed.record" .embed;; 150 153 "app.bsky.embed.record");; ··· 155 158 bapBskyInternal_convertToRecordWithMedia media 156 159 l=.record;; 157 160 esac 158 - bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .embed.record$l 159 - bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.cid') .embed.record$l 161 + bapCYOR_str uri "$(echo "$bapBsky_temp" | jq -re '.uri')" .embed.record$l 162 + bapCYOR_str cid "$(echo "$bapBsky_temp" | jq -re '.cid')" .embed.record$l 160 163 return 0 161 164 } 162 165 163 166 function bapBsky_cyorAddExternalEmbed () { 164 167 if [ -z "$3" ] || [ -n "$4" ] && [ -z "$6" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 165 - if [ "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" = "app.bsky.embed.record" ]; then bapBskyInternal_convertToRecordWithMedia record; fi 166 - if [ "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" = "app.bsky.embed.recordWithMedia" ]; then local l=.media; fi 167 - bapCYOR_str \$type app.bsky.embed.external .embed$l 168 - bapCYOR_str uri "$1" .embed$l.external 169 - bapCYOR_str title "$2" .embed$l.external 170 - bapCYOR_str description "$3" .embed$l.external 168 + if [ "$(echo "$bap_cyorRecord" | jq -r ".embed.[\"\$type\"]")" = "app.bsky.embed.record" ]; then bapBskyInternal_convertToRecordWithMedia record; fi 169 + if [ "$(echo "$bap_cyorRecord" | jq -r ".embed.[\"\$type\"]")" = "app.bsky.embed.recordWithMedia" ]; then local l=.media; fi 170 + bapCYOR_str \$type app.bsky.embed.external ".embed$l" 171 + bapCYOR_str uri "$1" ".embed$l.external" 172 + bapCYOR_str title "$2" ".embed$l.external" 173 + bapCYOR_str description "$3" ".embed$l.external" 171 174 if [ -n "$4" ]; then 172 - bapCYOR_str \$type blob .embed$l.external.thumb 173 - bapCYOR_str \$link "$4" .embed$l.external.thumb.ref 174 - bapCYOR_str mimeType "$5" .embed$l.external.thumb 175 - bapCYOR_add size $6 .embed$l.external.thumb 175 + bapCYOR_str \$type blob ".embed$l.external.thumb" 176 + bapCYOR_str \$link "$4" ".embed$l.external.thumb.ref" 177 + bapCYOR_str mimeType "$5" ".embed$l.external.thumb" 178 + bapCYOR_add size "$6" ".embed$l.external.thumb" 176 179 fi 177 180 } 178 181 182 + #shellcheck disable=SC2120 179 183 function bapBsky_submitPost () { 180 - bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 181 - bap_postRecord "$bap_cyorRecord" || return $? 184 + if ! echo "$bap_cyorRecord" | jq -re .createdAt > /dev/null; then bapCYOR_str createdAt "$(bap_generateDatetime)"; fi 185 + bapBskyErrVerb "submitting record..." 186 + bap_postRecord "$bap_cyorRecord" "$1" "$2" || return $? 182 187 bap_cyorRecord= 183 - uri=$(echo $bap_result | jq -r .uri) 184 - cid=$(echo $bap_result | jq -r .cid) 188 + uri=$(echo "$bap_result" | jq -r .uri) 189 + cid=$(echo "$bap_result" | jq -r .cid) 185 190 return 0 186 191 } 187 192 ··· 189 194 if [ -z "$1" ]; then bapBskyErr "fatal: No argument given to post"; return 1; fi 190 195 bapBsky_cyorInit 191 196 bapCYOR_str text "$1" 192 - if [ -n "$2" ]; then bapBsky_cyorAddLangs $2; fi 197 + if [ -n "$2" ]; then bapBsky_cyorAddLangs "$2"; fi 193 198 bapBsky_submitPost || return $? 194 199 bapBskyEcho "Posted record at $uri" 195 200 return 0 196 201 } 197 202 198 - 199 203 function bapBsky_createRepost () { # arguments 1 is uri, 2 is cid. error codes same as postToBluesky 200 204 if [ -z "$2" ]; then bapBskyErr "fatal: Required argument missing"; return 1; fi 205 + if [ "$bapBsky_skipRepostChecks" != "1" ]; then 206 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 207 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.repost)" != "null" ]; then bapBskyErr "you already reposted this post!"; return 0; fi 208 + fi 201 209 bap_cyorRecord= 202 210 bapCYOR_str \$type app.bsky.feed.repost 203 - bapCYOR_str cid $2 .subject 204 - bapCYOR_str uri $1 .subject 211 + bapCYOR_str cid "$2" .subject 212 + bapCYOR_str uri "$1" .subject 213 + if [ "$3" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 205 214 bapBsky_submitPost || return $? 206 215 bapBskyEcho "Repost record at $uri" 207 216 return 0 ··· 223 232 # 7 - text 224 233 if [ -z "$5" ]; then bapBskyErr "fatal: more arguments required"; return 1; fi 225 234 bapBsky_cyorInit 226 - bapBsky_cyorAddImage 0 $1 $2 $3 $4 $5 "$6" 235 + bapBsky_cyorAddImage 0 "$1" "$2" "$3" "$4" "$5" "$6" 227 236 bapCYOR_str text "$7" 228 237 bapBsky_submitPost || return $? 229 238 bapBskyEcho "Posted record at $uri" ··· 238 247 } 239 248 240 249 function bapBsky_prepareVideoIndirect () { 241 - bapBsky_prepareVideo "$@" 242 - return $? 243 - } 244 - 245 - function bapBsky_prepareVideo () { 246 - # stub, will actually talk to bluesky video service in the future 247 250 # $1 is file 248 251 # $2 is mime (like bap_postBlobToPDS) 249 252 if [ -z "$2" ]; then bapBskyErr "fatal: Required argument missing"; return 1; fi 250 - if [ "$2" != "video/mp4" ]; then if [ "$bapBsky_allowLegacyPre2" != "1" ]; then bapBskyErr "videos must be mp4"; return 1; else bapBskyErr "warning: videos are only supposed to be mp4. this will be blocked in a future version"; fi; fi 253 + if [ "$2" != "video/mp4" ]; then bapBskyErr "videos must be mp4"; fi 251 254 bapBsky_checkVideo "$1" || return $? 255 + # bap_prepareImage "$1" 100000000 2147483647 2147483647 252 256 local uuid=$(uuidgen) 253 - if [ "$bapBsky_allowLegacyPre2" != "1" ]; then 254 - exiftool -all= "$1" -o "/tmp/$uuid.mp4" || return $? 255 - else 256 - cp "$1" "/tmp/$uuid.mp4" || return $? 257 - fi 257 + exiftool -all= "$1" -o "/tmp/$uuid.mp4" || return $? 258 258 bap_postBlobToPDS "/tmp/$uuid.mp4" "$2" || { bapBskyErr "warning: video upload failed"; rm "/tmp/$uuid.mp4"; return 1; } 259 259 rm "/tmp/$uuid.mp4" 260 - bap_imageWidth=$(exiftool -ImageWidth -s3 $1) 261 - bap_imageHeight=$(exiftool -ImageHeight -s3 $1) 262 - bapBskyEcho 'video "posted"' 260 + bap_imageWidth=$(exiftool -ImageWidth -s3 "$1") 261 + bap_imageHeight=$(exiftool -ImageHeight -s3 "$1") 262 + bapBskyEcho 'uploaded video' 263 263 return 0 264 264 } 265 265 266 266 function bapBsky_getVideoLimits () { 267 267 # TODO: figure out atproto-proxy for Lumi 268 268 local bap_result bapBsky_temp 269 - bapBsky_temp=$(bap_getServiceAuth did:web:video.bsky.app "" app.bsky.video.getUploadLimits) || return $? 270 - bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -H "Authorization: Bearer $bapBsky_temp" "https://video.bsky.app/xrpc/app.bsky.video.getUploadLimits") 271 - bapInternal_errorCheck $? bapBsky_videoLimits "fatal: failed to get video limits" || return $? 272 - if [ "$1" = "--raw" ]; then echo $bap_result; else 273 - if [ "$(echo $bap_result | jq -r .canUpload)" != "true" ]; then echo $bap_result | jq -r .message; return 1; fi 274 - echo $bap_result | jq -r .message 275 - echo "Can upload $(echo $bap_result | jq -r .remainingDailyVideos) more videos, $(echo $bap_result | jq -r .remainingDailyBytes) more bytes." 269 + bapBsky_temp=$(bap_getServiceAuth "did:web:$bapBsky_lumiURL" "" app.bsky.video.getUploadLimits) || return $? 270 + bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -H "Authorization: Bearer $bapBsky_temp" "https://$bapBsky_lumiURL/xrpc/app.bsky.video.getUploadLimits") 271 + bapInternal_errorCheck $? bapBsky_getvideoLimits "fatal: failed to get video limits" || return $? 272 + if [ "$1" = "--raw" ]; then echo "$bap_result"; else 273 + if [ "$(echo "$bap_result" | jq -r .canUpload)" != "true" ]; then echo "$bap_result" | jq -r .message; return 1; fi 274 + echo "$bap_result" | jq -r .message 275 + echo "Can upload $(echo "$bap_result" | jq -r .remainingDailyVideos) more videos, $(echo "$bap_result" | jq -r .remainingDailyBytes) more bytes." 276 276 return 0 277 277 fi 278 278 } 279 279 280 + # function bapBsky_videoUpload () { 281 + # local bap_resultcode 282 + # bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H "Authorization: Bearer $1" -H "Content-Type: $3" --data-binary @"$2" "https://$bapBsky_lumiURL/xrpc/app.bsky.video.uploadVideo") 283 + # bap_resultcode=$? 284 + # # BskyVideo can throw HTTP 409 if it's already done, check for that 285 + # if [ "$(echo $bap_result | jq -r .jobStatus.error)" = "already_exists" ]; then bap_resultcode=0; fi 286 + # bapInternal_errorCheck $bap_resultcode bapBsky_videoUpload "fatal: failed to upload video" || return $? 287 + # # give job id stuff 288 + # bapBsky_jobId=$(echo "$bap_result" | jq -r .jobStatus.jobId) 289 + # return 0 290 + # } 291 + # 292 + # function bapBsky_videoStatus () { 293 + # # no auth needed? 294 + # curl --fail-with-body -s -A "$bap_curlUserAgent" "https://$bapBsky_lumiURL/xrpc/app.bsky.video.getJobStatus?jobId=$1" 295 + # } 296 + 297 + function bapBsky_prepareVideo () { 298 + # if [ -z "$2" ]; then bapBskyErr "fatal: Required argument missing"; return 1; fi 299 + # bapBsky_checkVideo "$1" || return $? 300 + # local bapBsky_temp 301 + # #bapBsky_temp=$(bap_getServiceAuth did:web:video.bsky.app 900 com.atproto.server.uploadBlob) || return $? 302 + # bapBsky_temp=$(bap_getServiceAuth did:web:$(echo $savedPDS | sed 's|https://||g') 900 com.atproto.server.uploadBlob) || return $? 303 + # bapBsky_videoUpload "$bapBsky_temp" "$1" "$2" || return $? 304 + # while :; do 305 + # bapBsky_temp=$(bapBsky_videoStatus "$bapBsky_jobId" | jq -r '.jobStatus.state') || { bapBskyErr "unexpected response from videoStatus"; return 1; } 306 + # if [ "$bapBsky_temp" = "JOB_STATUS_COMPLETED" ]; then break; fi 307 + # if [ "$bapBsky_temp" = "JOB_STATUS_FAILED" ]; then bapBskyErr "the video failed to process"; return 1; fi 308 + # sleep 1 309 + # done 310 + # bap_postedBlob=$(echo $bapBsky_temp | jq -r .jobStatus.blob) 311 + # bap_postedMime="video/mp4" 312 + # bap_imageWidth=$(exiftool -ImageWidth -s3 $1) 313 + # bap_imageHeight=$(exiftool -ImageHeight -s3 $1) 314 + # return 0 315 + bapBskyErr "function not implemented. try using bapBsky_prepareVideoIndirect" 316 + return 1 317 + } 318 + 280 319 function bapBsky_postVideo () { 281 320 # param: 282 321 # 1-5 - see bapBsky_cyorAddVideo ··· 284 323 if [ -z "$4" ]; then bapBskyErr "fatal: more arguments required"; return 1; fi 285 324 bapBsky_cyorInit 286 325 bapCYOR_str text "$6" 287 - bapBsky_cyorAddVideo $1 $2 $3 $4 "$5" 326 + bapBsky_cyorAddVideo "$1" "$2" "$3" "$4" "$5" 288 327 bapBsky_submitPost || return $? 289 328 bapBskyEcho "Posted record at $uri" 290 329 return 0 291 330 } 292 331 293 - # Compatibility stubs 294 - # Legacy scripts need to load bap-bsky.sh to use them. 295 - 296 - function bapBsky_stubWarn () { 297 - if [ "${FUNCNAME[1]}" != "bap_postToBluesky" ] && [ "$bapBsky_allowLegacyPre2" != "1" ]; then >&2 echo "${FUNCNAME[1]}: command not found"; return 127; fi 298 - bapBskyErr "warn: function ${FUNCNAME[1]} was renamed. call $1 instead" 299 - if [ "${FUNCNAME[1]}" != "bap_postToBluesky" ]; then bapBskyErr "warn: this compatibility stub is deprecated and will be removed in a future version"; fi 300 - } 301 - 302 - function bapCYOR_bskypost () { 303 - bapBsky_stubWarn bapBsky_cyorInit || return $? 304 - bapBsky_cyorInit 305 - return $? 306 - } 307 - 308 - # This replaces the bash-atproto native function 309 - function bap_postToBluesky () { 310 - bapBsky_stubWarn bapBsky_createPost 311 - bapBsky_createPost "$@" 312 - return $? 313 - } 314 - 315 - function bap_repostToBluesky () { 316 - bapBsky_stubWarn bapBsky_createRepost || return $? 317 - bapBsky_createRepost $* 318 - return $? 332 + function bapBsky_verifyActor () { 333 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 334 + local bap_cyorRecord bapBsky_temp bapBsky_temp2 335 + bapBsky_temp2=$(bap_getDID "$1") || return $? 336 + bapBsky_temp=$(bap_getRecord "at://$bapBsky_temp2/app.bsky.actor.profile/self") || { bapBskyErr "failed to resolve profile for verification"; return 1; } 337 + bap_cyorRecord= 338 + bapCYOR_str \$type "app.bsky.graph.verification" 339 + bapCYOR_str handle "$(bap_resolveHandle "$bapBsky_temp2")" 340 + bapCYOR_str subject "$bapBsky_temp2" 341 + bapCYOR_str displayName "$(echo "$bapBsky_temp" | jq -r .value.displayName)" 342 + if [ "$2" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 343 + bapBsky_submitPost || return $? 344 + bapBskyEcho "verified $(echo "$bapBsky_temp" | jq -r .value.displayName)" 345 + return 0 319 346 } 320 347 321 - function bap_prepareImageForBluesky () { 322 - bapBsky_stubWarn bapBsky_prepareImage || return $? 323 - bapBsky_prepareImage "$@" 324 - return $? 325 - } 326 - 327 - function bap_postImageToBluesky () { 328 - bapBsky_stubWarn bapBsky_postImage || return $? 329 - bapBsky_postImage "$@" 330 - return $? 348 + function bapBsky_getProfile () { 349 + if [ -z "$savedAccess" ]; then bapBskyErr "needs auth!"; return 1; fi 350 + if ! bapInternal_validateDID "$1" 2>/dev/null && ! bapInternal_validateHandle "$1" 2>/dev/null; then bapBskyErr "bad identifier"; return 1; fi 351 + local bap_result 352 + bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -H "Authorization: Bearer $savedAccess" -H "atproto-proxy: $bapBsky_bskyAppViewDID" "$savedPDS/xrpc/app.bsky.actor.getProfile?actor=$1") 353 + bapInternal_errorCheck $? bapBsky_getProfile "failed to get profile for $1" || return $? 354 + echo "$bap_result" 355 + return 0 331 356 } 332 357 333 - function bap_checkVideoForBluesky () { 334 - bapBsky_stubWarn bapBsky_checkVideo || return $? 335 - bapBsky_checkVideo "$@" 336 - return $? 358 + function bapBsky_followActor () { 359 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 360 + local bap_cyorRecord='' bapBsky_temp 361 + bapBsky_temp=$(bapBsky_getProfile "$1") || return $? 362 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.blockedBy)" = "true" ]; then bapBskyErr "can't follow: $(echo "$bapBsky_temp" | jq -r .displayName) is blocking you!"; return 2; fi 363 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.blockingByList)" != "null" ]; then bapBskyErr "can't follow: you are blocking $(echo "$bapBsky_temp" | jq -r .displayName) by list $(echo "$bapBsky_temp" | jq -r .viewer.blockingByList.name)!"; return 3; fi 364 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.blocking)" != "null" ]; then bapBskyErr "can't follow: you are blocking $(echo "$bapBsky_temp" | jq -r .displayName)!"; return 3; fi 365 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.following)" != "null" ]; then bapBskyErr "already following $(echo "$bapBsky_temp" | jq -r .displayName)"; return 0; fi 366 + if [ "$(echo "$bapBsky_temp" | jq -r .did)" = "$savedDID" ]; then bapBskyErr "you can't follow yourself!"; return 1; fi 367 + bapCYOR_str \$type "app.bsky.graph.follow" 368 + bapCYOR_str subject "$(bap_getDID "$1" || return 1)" 369 + if [ "$2" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 370 + bapBsky_submitPost || return $? 371 + bapBskyEcho "now following $(echo "$bapBsky_temp" | jq -r .displayName)" 372 + return 0 337 373 } 338 374 339 - function bap_prepareVideoForBluesky () { 340 - bapBsky_stubWarn bapBsky_prepareVideo || return $? 341 - bapBsky_prepareVideo "$@" 342 - return $? 375 + function bapBsky_getPost () { 376 + if [ -z "$savedAccess" ]; then bapBskyErr "needs auth!"; return 1; fi 377 + if [ "$(echo "$1" | cut -d '/' -f 4)" != "app.bsky.feed.post" ]; then bapBskyErr "this is not a post!"; return 1; fi 378 + local bap_result 379 + bapBskyErrVerb "looking up post..." 380 + bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -H "Authorization: Bearer $savedAccess" -H "atproto-proxy: $bapBsky_bskyAppViewDID" "$savedPDS/xrpc/app.bsky.feed.getPosts?uris=$(echo -n "$1" | jq -s -R -r @uri)") 381 + bapInternal_errorCheck $? bapBsky_getPost "failed to get post with uri $1" || return $? 382 + echo "$bap_result" | jq -r .posts.[0] 383 + if [ "$bap_result" = "null" ] && [ "$2" = "-e" ]; then bapBskyErr "failed to resolve post. it may not exist or there may be a blocking relationship."; return 2; fi 384 + return 0 343 385 } 344 386 345 - function bap_postVideoToBluesky () { 346 - bapBsky_stubWarn bapBsky_postVideo || return $? 347 - bapBsky_postVideo "$@" 348 - return $? 387 + function bapBsky_likePost () { 388 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 389 + local bap_cyorRecord='' bapBsky_temp 390 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 391 + if [ "$(echo "$bapBsky_temp" | jq -r .viewer.like)" != "null" ]; then bapBskyErr "you already liked this post!"; return 0; fi 392 + bapCYOR_str \$type "app.bsky.feed.like" 393 + bapCYOR_str cid "$(echo "$bapBsky_temp" | jq -r .cid)" .subject 394 + bapCYOR_str uri "$(echo "$bapBsky_temp" | jq -r .uri)" .subject 395 + if [ "$2" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 396 + bapBsky_submitPost || return $? 349 397 }
+30 -22
bap-extra.sh
··· 2 2 # SPDX-License-Identifier: MIT 3 3 # bap-extra.sh: Other functions that might be useful 4 4 if [ -z "$bap_internalVersion" ]; then >&2 echo "bash-atproto not loaded?"; return 127; fi 5 - if [ "$bap_internalVersion" != "3" ] || ! [ "$bap_internalMinorVer" -ge "1" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 5 + if [ "$bap_internalVersion" != "4" ] || ! [ "$bap_internalMinorVer" -ge "-1" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 6 6 7 - bapExt_internalVersion=1 7 + bapExt_internalVersion=2 8 8 bapExt_internalMinorVer=1 9 9 10 10 function bapExtErr () { 11 - >&2 echo "bap-extra: $*" 11 + >&2 echo "bap-extra: $*" 12 12 } 13 13 14 14 function bapExtEcho () { 15 - if [ ! "$bap_verbosity" -ge 1 ]; then return 0; fi 16 - echo "bap-extra: $*" 15 + if [ ! "$bap_verbosity" -ge 1 ]; then return 0; fi 16 + echo "bap-extra: $*" 17 17 } 18 18 19 19 function bapExt_authWithATFileCreds () { 20 - # Auth with atfile credentials for your convenience 21 - if [ -f "$HOME/.config/atfile.env" ]; then while IFS= read -r line; do declare -g "$line"; done < "$HOME/.config/atfile.env" 22 - else bapExtErr "no ATFile credentials file to load!"; return 1; fi 23 - if [ -z "$ATFILE_USERNAME" ] || [ -z "$ATFILE_PASSWORD" ]; then bapExtErr "ATFile credentials not found!"; fi 24 - bap_findPDS $(bap_resolveDID $ATFILE_USERNAME) || { bapExtErr "Couldn't resolve PDS!"; return 1; } 25 - bap_getKeys $ATFILE_USERNAME $ATFILE_PASSWORD || { bapExtErr "Couldn't log in with ATFile credentials!"; return 1; } 26 - bapInternal_loadFromJwt 27 - ATFILE_USERNAME= ATFILE_PASSWORD= 28 - bapExtEcho "Auth successful. Use bap_closeSession when done" 29 - return 0 30 - } 31 - 32 - function bapExt_fastLogin () { 33 - if [ "$2" = "" ]; then bapExtErr "Required argument missing"; return 1; fi 34 - bap_findPDS $(bap_resolveDID $1) || return $? 35 - bap_getKeys $(bap_resolveDID $1) $2 || return $? 36 - bapInternal_loadFromJwt 20 + # Auth with atfile credentials for your convenience 21 + if [ -f "$HOME/.config/atfile.env" ]; then while IFS= read -r line; do declare -g "$line"; done < "$HOME/.config/atfile.env" 22 + else bapExtErr "no ATFile credentials file to load!"; return 1; fi 23 + if [ -z "$ATFILE_USERNAME" ] || [ -z "$ATFILE_PASSWORD" ]; then bapExtErr "ATFile credentials not found!"; fi 24 + bap_getKeys "$ATFILE_USERNAME" "$ATFILE_PASSWORD" || { bapExtErr "Couldn't log in with ATFile credentials!"; return 1; } 25 + ATFILE_USERNAME='' ATFILE_PASSWORD='' 26 + bapExtEcho "Auth successful. Use bap_closeSession when done" 37 27 return 0 38 28 } 39 29 ··· 45 35 if ! type uuidgen >/dev/null 2>&1; then problem=2; bapExtErr "missing uuidgen"; fi 46 36 if ! type jq >/dev/null 2>&1; then problem=1; bapExtErr "missing jq"; fi 47 37 if ! type curl >/dev/null 2>&1; then problem=1; bapExtErr "missing curl"; fi 38 + if ! type dig >/dev/null 2>&1; then problem=1; bapExtErr "missing dig"; fi 48 39 # if you can load this function, you have bash 49 40 50 41 case "$problem" in ··· 58 49 bapExtEcho "There are no bash-atproto dependency issues on this system.";; 59 50 esac 60 51 } 52 + 53 + function bapExt_bskyCancelActor () { 54 + if [ -z "$1" ]; then bapExtErr "error: Required argument missing"; return 1; fi 55 + local bap_cyorRecord bapExt_temp bapExt_temp2 56 + bapExt_temp2=$(bap_getDID "$1") || return $? 57 + bapExt_temp=$(bap_getRecord "at://$bapExt_temp2/app.bsky.actor.profile/self") || { bapExtErr "failed to resolve profile for cancelation"; return 1; } 58 + bap_cyorRecord= 59 + bapCYOR_str \$type "app.bsky.graph.cancellation" 60 + bapCYOR_str handle "$(bap_resolveHandle "$bapExt_temp2")" 61 + bapCYOR_str subject "$bapExt_temp2" 62 + bapCYOR_str displayName "$(echo "$bapExt_temp" | jq -r .value.displayName)" 63 + bapCYOR_str createdAt "$(bap_generateDatetime)" 64 + bap_postRecord "$bap_cyorRecord" || return $? 65 + uri=$(echo "$bap_result" | jq -r .uri) 66 + cid=$(echo "$bap_result" | jq -r .cid) 67 + bapExtEcho "canceled $(echo "$bapExt_temp" | jq -r .value.displayName)" 68 + }
+54 -29
bap-sprk.sh
··· 3 3 # bap-sprk.sh: Spark the revolution...from the command line! 4 4 # The PDS is not capable of verifying Spark lexicon at the moment, be careful! 5 5 # This code has not been thoroughly tested. Use at your own risk! 6 + # TODO Replies still use .text not .caption.text 6 7 if [ -z "$bap_internalVersion" ]; then >&2 echo "bash-atproto not loaded?"; return 127; fi 7 - if [ "$bap_internalVersion" != "3" ] || ! [ "$bap_internalMinorVer" -ge "0" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 8 + if [ "$bap_internalVersion" != "4" ] || ! [ "$bap_internalMinorVer" -ge "-1" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 8 9 9 10 10 11 bapSprk_internalVersion=2 11 - bapSprk_internalMinorVer=0 12 + bapSprk_internalMinorVer=1 12 13 13 14 function bapSprk_err () { 14 15 >&2 echo "bap-sprk: $*" ··· 22 23 function bapSprk_cyorInit () { 23 24 bap_cyorRecord="{}" 24 25 bapCYOR_str \$type so.sprk.feed.post 25 - bapCYOR_str text "" 26 + bapCYOR_str text "" .caption 26 27 } 27 28 28 29 function bapSprk_cyorAddImage () { 29 30 # Lexicon has alt text but no image dimensions 30 31 # 1 image, 2 blob, 3 mime, 4 size, 5 alt 31 - if [ -z "$4" ]; then bapSprkErr "error: Required argument missing"; return 1; fi 32 - bapCYOR_str \$type so.sprk.embed.images .embed 33 - bapCYOR_str alt "$5" .embed.images.[$1] 34 - bapCYOR_str \$type so.sprk.embed.images#image .embed.images.[$1] 35 - bapCYOR_str \$type blob .embed.images.[$1].image 36 - bapCYOR_str \$link $2 .embed.images.[$1].image.ref 37 - bapCYOR_str mimeType "$3" .embed.images.[$1].image 38 - bapCYOR_add size $4 .embed.images.[$1].image 39 - #bapCYOR_add width $6 .embed.images.[$1].aspectRatio 40 - #bapCYOR_add height $7 .embed.images.[$1].aspectRatio 32 + if [ -z "$4" ]; then bapSprk_err "error: Required argument missing"; return 1; fi 33 + bapCYOR_str \$type so.sprk.media.images .media 34 + bapCYOR_str alt "$5" ".media.images.[$1]" 35 + bapCYOR_str \$type so.sprk.embed.images#image ".media.images.[$1]" 36 + bapCYOR_str \$type blob ".media.images.[$1].image" 37 + bapCYOR_str \$link "$2" ".media.images.[$1].image.ref" 38 + bapCYOR_str mimeType "$3" ".media.images.[$1].image" 39 + bapCYOR_add size "$4" ".media.images.[$1].image" 40 + #bapCYOR_add width $6 ".media.images.[$1].aspectRatio" 41 + #bapCYOR_add height $7 ".media.images.[$1].aspectRatio" 41 42 } 42 43 43 44 function bapSprk_cyorAddVideo () { 44 - if [ -z "$3" ]; then bapSprkErr "error: Required argument missing"; return 1; fi 45 - bapCYOR_str \$type so.sprk.embed.video .embed 46 - bapCYOR_str alt "$4" .embed 47 - bapCYOR_str \$type blob .embed.video 48 - bapCYOR_str \$link $1 .embed.video.ref 49 - bapCYOR_str mimeType "$3" .embed.video 50 - bapCYOR_add size $2 .embed.video 45 + if [ -z "$3" ]; then bapSprk_err "error: Required argument missing"; return 1; fi 46 + bapCYOR_str \$type so.sprk.media.video .media 47 + bapCYOR_str alt "$4" .media 48 + bapCYOR_str \$type blob .media.video 49 + bapCYOR_str \$link "$1" .media.video.ref 50 + bapCYOR_str mimeType "$3" .media.video 51 + bapCYOR_add size "$2" .media.video 51 52 } 52 53 53 54 function bapSprk_submitPost () { 54 - bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 55 - bap_postRecord "$bap_cyorRecord" || return $? 55 + bapCYOR_str createdAt "$(bap_generateDatetime)" 56 + bap_postRecord "$bap_cyorRecord" "$1" "$2" || return $? 56 57 bap_cyorRecord= 57 - uri=$(echo $bap_result | jq -r .uri) 58 - cid=$(echo $bap_result | jq -r .cid) 58 + uri=$(echo "$bap_result" | jq -r .uri) 59 + cid=$(echo "$bap_result" | jq -r .cid) 59 60 return 0 60 61 } 61 62 62 63 function bapSprk_prepareImage () { 63 64 # 5MB image limit: at://sprk.so/app.bsky.feed.post/3lipdqef2k22n 64 - # Get rid of dimensions liimt with really big numbers 65 + # Get rid of dimensions limit with really big numbers 65 66 bap_prepareImage "$1" 5000000 2147483647 2147483647 66 67 return $? 67 68 } ··· 76 77 # 5 post text 77 78 if [ -z "$3" ]; then bapSprk_err "fatal: more arguments required"; return 1; fi 78 79 bapSprk_cyorInit 79 - bapCYOR_str text "$5" 80 - bapSprk_cyorAddVideo $1 $2 $3 "$4" 80 + bapCYOR_str text "$5" .caption 81 + bapSprk_cyorAddVideo "$1" "$2" "$3" "$4" 81 82 bapSprk_submitPost "$bap_cyorRecord" || return $? 82 83 bapSprk_echo "Posted record at $uri" 83 84 return 0 ··· 94 95 # 5 - text 95 96 if [ -z "$3" ]; then bapSprk_err "fatal: more arguments required"; return 1; fi 96 97 bapSprk_cyorInit 97 - bapCYOR_str text "$5" 98 - bapSprk_cyorAddImage 0 $1 $2 $3 "$4" "$5" 98 + bapCYOR_str text "$5" .caption 99 + bapSprk_cyorAddImage 0 "$1" "$2" "$3" "$4" "$5" 99 100 bapSprk_submitPost "$bap_cyorRecord" || return $? 100 101 bapSprk_echo "Posted record at $uri" 101 102 return 0 102 103 } 104 + 105 + function bapSprkInternal_cyorGetReplyRoot () { 106 + if [ -z "$1" ]; then bapSprk_err "error: Required argument missing"; return 1; fi 107 + bapSprk_temp=$(bap_getRecord "$1") || return $? 108 + if echo "$bapSprk_temp" | jq -re '.value.reply.root.uri' > /dev/null; then 109 + # copy values 110 + bapCYOR_str uri "$(echo "$bapSprk_temp" | jq -re '.value.reply.root.uri')" .reply.root 111 + bapCYOR_str cid "$(echo "$bapSprk_temp" | jq -re '.value.reply.root.cid')" .reply.root 112 + else 113 + # we just wasted time and bandwidth! 114 + bapCYOR_str uri "$(echo "$bapSprk_temp" | jq -re '.uri')" .reply.root 115 + bapCYOR_str cid "$(echo "$bapSprk_temp" | jq -re '.cid')" .reply.root 116 + fi 117 + return 0 118 + } 119 + 120 + function bapSprk_cyorAddReply () { 121 + if [ -z "$1" ]; then bapSprk_err "error: Required argument missing"; return 1; fi 122 + bapSprk_temp=$(bap_getRecord "$1") || return $? 123 + bapCYOR_str uri "$(echo "$bapSprk_temp" | jq -re '.uri')" .reply.parent 124 + bapCYOR_str cid "$(echo "$bapSprk_temp" | jq -re '.cid')" .reply.parent 125 + bapSprkInternal_cyorGetReplyRoot "$1" 126 + return 0 127 + }
+138 -109
bash-atproto.sh
··· 1 1 #!/bin/bash 2 2 # SPDX-License-Identifier: MIT 3 - bap_internalVersion=3 4 - bap_internalMinorVer=3 3 + # shellcheck disable=SC2034 4 + bap_internalVersion=4 5 + bap_internalMinorVer=-1 5 6 6 7 # you can change these 7 8 bap_plcDirectory=https://plc.directory 8 - bap_handleResolveURL=https://public.api.bsky.app 9 - bap_curlUserAgent="curl/$(curl -V | awk 'NR==1{print $2}') bash-atproto/$bap_internalVersion.$bap_internalMinorVer-$(git -C $(dirname $BASH_SOURCE) -c safe.directory=$(cd $(dirname $BASH_SOURCE); pwd) describe --always --dirty)" 9 + bap_curlUserAgent="curl/$(curl -V | awk 'NR==1{print $2}') bash-atproto/$bap_internalVersion.$bap_internalMinorVer-$(git -C "$(dirname "${BASH_SOURCE[0]}")" -c safe.directory="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" describe --always --dirty 2>/dev/null)" 10 10 bap_chmodSecrets=1 11 11 bap_verbosity=1 12 + bap_disableOptionalChecks=0 13 + bap_dryRun=0 12 14 13 15 function baperr () { 14 16 >&2 echo "bash-atproto: $*" ··· 30 32 } 31 33 32 34 function bap_decodeJwt () { 33 - bap_jwt="$(echo $1 | cut -d '.' -f 2 \ 35 + bap_jwt="$(echo "$1" | cut -d '.' -f 2 \ 34 36 | sed 's/$/====/' | fold -w 4 | sed '$ d' | tr -d '\n' | tr '_-' '/+' \ 35 37 | base64 -d | jq -re)" || { baperr "not a jwt"; return 1; } 36 38 # 1: fetch JWT payload 2: pad and convert to base64 3: decode ··· 38 40 } 39 41 40 42 function bapInternal_loadFromJwt () { 41 - savedDID="$(echo $bap_jwt | jq -r .sub)" 42 - savedPDS="https://$(echo $bap_jwt | jq -r .aud | sed 's/did:web://g')" 43 - savedAccessTimestamp="$(echo $bap_jwt | jq -r .iat)" #deprecated 44 - savedAccessExpiry="$(echo $bap_jwt | jq -r .exp)" 43 + savedDID="$(echo "$bap_jwt" | jq -r .sub)" 44 + savedPDS="https://$(echo "$bap_jwt" | jq -r .aud | sed 's/did:web://g')" 45 + savedAccessTimestamp="$(echo "$bap_jwt" | jq -r .iat)" #deprecated 46 + savedAccessExpiry="$(echo "$bap_jwt" | jq -r .exp)" 47 + } 48 + 49 + function bapInternal_verifyStatus () { 50 + if [ "$bap_disableOptionalChecks" = "1" ]; then return 0; fi 51 + if [ "$(echo "$bap_result" | jq -r .active)" = "false" ]; then 52 + baperr "warning: account is inactive" 53 + if [ -n "$(echo "$bap_result" | jq -r .status)" ]; then baperr "pds said: $(echo "$bap_result" | jq -r .status)"; else baperr "no reason was given for the account not being active"; fi 54 + return 115 55 + fi 45 56 } 46 57 47 58 function bap_loadSecrets () { ··· 55 66 56 67 function bap_saveSecrets () { 57 68 bapecho 'Updating secrets' 58 - echo 'savedAccess='$savedAccess > "$1" 59 - echo 'savedRefresh='$savedRefresh >> "$1" 69 + echo "savedAccess=$savedAccess" > "$1" 70 + echo "savedRefresh=$savedRefresh" >> "$1" 60 71 if [ "$bap_chmodSecrets" != "0" ]; then chmod 600 "$1"; fi 61 72 return 0 62 73 } 63 74 64 75 function bapInternal_processAPIError () { 65 - baperr 'Function' $1 'encountered an API error' 66 - APIErrorCode=$(echo ${!2} | jq -r .error) 67 - APIErrorMessage=$(echo ${!2} | jq -r .message) 68 - baperr 'Error code:' $APIErrorCode 69 - baperr 'Message:' $APIErrorMessage 76 + baperr "Function $1 encountered an API error" 77 + APIErrorCode=$(echo "${!2}" | jq -r .error) 78 + APIErrorMessage=$(echo "${!2}" | jq -r .message) 79 + baperr "Error code: $APIErrorCode" 80 + baperr "Message: $APIErrorMessage" 70 81 } 71 82 72 83 function bapInternal_errorCheck () { ··· 75 86 22) 76 87 if [ -n "$3" ]; then baperr "$3"; fi 77 88 if ! jq -e . >/dev/null 2>&1 <<<"$bap_result"; then baperr "the server did not respond with valid JSON"; return 1; fi 78 - APIErrorCode=$(echo $bap_result | jq -r .error) 79 - if ! [ "$APIErrorCode" = "ExpiredToken" ]; then bapInternal_processAPIError $2 bap_result; return 1; fi 89 + APIErrorCode=$(echo "$bap_result" | jq -r .error) 90 + if ! [ "$APIErrorCode" = "ExpiredToken" ]; then bapInternal_processAPIError "$2" bap_result; return 1; fi 80 91 baperr 'the token needs to be refreshed' 81 92 return 2;; 82 93 *) ··· 86 97 esac 87 98 } 88 99 89 - function bapInternal_verifyStatus () { 90 - if [ "$(echo $bap_result | jq -r .active)" = "false" ]; then 91 - baperr "warning: account is inactive" 92 - if [ -n "$(echo $bap_result | jq -r .status)" ]; then baperr "pds said: $(echo $bap_result | jq -r .status)"; else baperr "no reason was given for the account not being active"; fi 93 - return 115 94 - fi 100 + function bapInternal_validateDID () { 101 + if ! [[ "$1" =~ ^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$ ]]; then baperr "fatal: input not a did"; return 1; fi 102 + return 0 95 103 } 96 104 97 - function bapInternal_validateDID () { 98 - if ! [[ "$1" =~ ^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$ ]]; then baperr "fatal: input not a did"; return 1; fi 105 + function bapInternal_validateHandle () { 106 + if ! [[ "$1" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]; then baperr "fatal: input not a handle"; return 1; fi 99 107 return 0 100 108 } 101 109 102 110 function bap_getKeys () { # 1: failure 2: user error 103 111 if [ -z "$2" ]; then baperr "No app password was passed"; return 2; fi 112 + local lSavedPDS=$3 113 + if [ -z "$lSavedPDS" ]; then lSavedPDS=$(bap_resolvePDS "$(bap_getDID "$1")") || return $?; fi 104 114 bapecho 'fetching keys' 105 - bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H 'Content-Type: application/json' -d "{ \"identifier\": \"$1\", \"password\": \"$2\" }" "$savedPDS/xrpc/com.atproto.server.createSession") 115 + bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H 'Content-Type: application/json' -d "{\"identifier\":\"$1\",\"password\":\"$2\"}" "$lSavedPDS/xrpc/com.atproto.server.createSession") 106 116 bapInternal_errorCheck $? bap_getKeys "fatal: failed to authenticate" || return $? 107 117 bapecho secured the keys! 108 - savedAccess=$(echo $bap_result | jq -r .accessJwt) 109 - savedRefresh=$(echo $bap_result | jq -r .refreshJwt) 118 + savedAccess=$(echo "$bap_result" | jq -r .accessJwt) 119 + savedRefresh=$(echo "$bap_result" | jq -r .refreshJwt) 110 120 # we don't care about the handle 111 - bap_decodeJwt $savedAccess 112 - if [ "$(echo $bap_jwt | jq -r .scope)" = "com.atproto.access" ]; then baperr "warning: this is not an app password"; fi 121 + bap_decodeJwt "$savedAccess" 122 + if [ "$(echo "$bap_jwt" | jq -r .scope)" = "com.atproto.access" ]; then baperr "warning: this is not an app password"; fi 123 + bapInternal_loadFromJwt 113 124 bapInternal_verifyStatus || return $? 114 125 return 0 115 126 } ··· 119 130 bapecho 'Trying to refresh keys...' 120 131 bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H "Authorization: Bearer $savedRefresh" "$savedPDS/xrpc/com.atproto.server.refreshSession") 121 132 bapInternal_errorCheck $? bap_refreshKeys "fatal: failed to refresh keys!" || return $? 122 - savedAccess=$(echo $bap_result | jq -r .accessJwt) 123 - savedRefresh=$(echo $bap_result | jq -r .refreshJwt) 124 - bap_decodeJwt $savedAccess 133 + savedAccess=$(echo "$bap_result" | jq -r .accessJwt) 134 + savedRefresh=$(echo "$bap_result" | jq -r .refreshJwt) 135 + bap_decodeJwt "$savedAccess" 136 + bapInternal_loadFromJwt 125 137 bapInternal_verifyStatus || return $? 126 138 return 0 127 139 } ··· 130 142 if [ -z "$savedRefresh" ]; then baperr "need refresh token to close session"; return 1; fi 131 143 bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H "Authorization: Bearer $savedRefresh" "$savedPDS/xrpc/com.atproto.server.deleteSession") 132 144 bapInternal_errorCheck $? bap_closeSession "error: failed to delete session" || return $? 133 - savedAccess= savedRefresh= 145 + savedAccess='' savedRefresh='' savedAccessTimestamp='' savedAccessExpiry='' 134 146 bapecho "session closed successfully" 135 147 return 0 136 148 } ··· 169 181 # doesn't handle special names atm 170 182 if [ -z "$1" ] || [ -z "$bap_cyorRecord" ]; then baperr "nothing to remove"; return 1; fi 171 183 local bap_temp 172 - bap_temp=$(echo $bap_cyorRecord | jq -c "del(.$1)") || return $? 184 + bap_temp=$(echo "$bap_cyorRecord" | jq -c "del(.$1)") || return $? 173 185 bap_cyorRecord=$bap_temp 174 186 return $? 175 187 } 176 188 177 189 function bapInternal_finalizeRecord () { 178 190 if ! jq -e . >/dev/null <<<"$1"; then baperr "can't finalize: JSON parse error"; return 1; fi 179 - bap_finalRecord="{\"collection\": $(echo $1 | jq -c '.["$type"]'), \"repo\": \"$savedDID\", \"record\": $1}" 191 + local bap_temp 192 + if [ "$2" = "true" ]; then bap_temp=", \"validate\": true"; fi 193 + if [ "$2" = "false" ]; then bap_temp=", \"validate\": false"; fi 194 + if [ -n "$3" ]; then bap_temp="$bap_temp, \"rkey\": \"$3\""; fi 195 + bap_finalRecord="{\"collection\": $(echo "$1" | jq -c '.["$type"]'), \"repo\": \"$savedDID\", \"record\": $1$bap_temp}" 180 196 if ! jq -e . >/dev/null <<<"$1"; then baperr "finalize: JSON parse error"; return 1; fi 181 197 return 0 182 198 } 183 199 184 200 function bap_postRecord () { 185 - bapInternal_finalizeRecord "$1" || { baperr "not posting because finalize failed"; return 1; } 201 + bapInternal_finalizeRecord "$1" "$2" "$3"|| { baperr "not posting because finalize failed"; return 1; } 202 + if [ "$bap_dryRun" = "1" ]; then 203 + bapecho "The following $(echo "$bap_finalRecord" | jq -r '.record.["$type"]') record would be sent to the PDS:" 204 + echo "$bap_finalRecord" | jq 205 + return 1 206 + fi 186 207 bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H "Authorization: Bearer $savedAccess" -H 'Content-Type: application/json' -d "$bap_finalRecord" "$savedPDS/xrpc/com.atproto.repo.createRecord") 187 208 bapInternal_errorCheck $? bap_postRecord "failed to post record" || return $? 188 209 return 0 189 210 } 190 211 191 - function bap_postToBluesky () { #1: exception 2: refresh required 192 - if [ -z "$1" ]; then baperr "fatal: No argument given to post"; return 1; fi 193 - bap_cyorRecord='{"$type":"app.bsky.feed.post"}' 194 - bapCYOR_str text "$1" 195 - if [ -n "$2" ]; then bapCYOR_add langs "[\"$2\"]"; fi 196 - bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 197 - bap_postRecord "$bap_cyorRecord" || return $? 198 - uri=$(echo $bap_result | jq -r .uri) 199 - cid=$(echo $bap_result | jq -r .cid) 200 - bapecho "Posted record at $uri" 201 - return 0 202 - } 203 - 204 212 function bapInternal_resizeImage () { 213 + if ! [[ "$mimetemp" =~ ^image ]]; then baperr "can't resize non-pictures"; return 2; fi 205 214 bapecho "need to resize image" 206 - convert /tmp/bash-atproto/$workfile -resize $1x$2 /tmp/bash-atproto/new-$workfile 207 - if ! [ "$?" = "0" ]; then baperr "fatal: convert failed!"; rm /tmp/bash-atproto/$workfile 2>/dev/null; return 1; fi 208 - mv -f /tmp/bash-atproto/new-$workfile /tmp/bash-atproto/$workfile 215 + convert "/tmp/bash-atproto/$workfile" -resize "$1"x"$2" "/tmp/bash-atproto/new-$workfile" || { baperr "fatal: convert failed!"; rm "/tmp/bash-atproto/$workfile" 2>/dev/null; return 1; } 216 + mv -f "/tmp/bash-atproto/new-$workfile" "/tmp/bash-atproto/$workfile" 209 217 } 210 218 211 219 function bapInternal_compressImage () { 220 + if ! [[ "$mimetemp" =~ ^image ]]; then baperr "can't compress non-pictures"; return 2; fi 212 221 bapecho "image is too big, trying to compress" 213 - convert /tmp/bash-atproto/$workfile -define jpeg:extent=$1 /tmp/bash-atproto/new-${workfile%.*}.jpg 214 - if [[ ! "$?" = "0" ]] || [[ $(stat -c %s /tmp/bash-atproto/new-${workfile%.*}.jpg) -gt $1 ]]; then baperr "fatal: error compressing image"; rm /tmp/bash-atproto/$workfile /tmp/bash-atproto/new-${workfile%.*}.jpg; return 1; fi 215 - rm /tmp/bash-atproto/$workfile 216 - mv -f /tmp/bash-atproto/new-${workfile%.*}.jpg /tmp/bash-atproto/${workfile%.*}.jpg 222 + convert "/tmp/bash-atproto/$workfile" -define jpeg:extent="$1" "/tmp/bash-atproto/new-${workfile%.*}.jpg" 223 + if [[ ! "$?" = "0" ]] || [[ $(stat -c %s "/tmp/bash-atproto/new-${workfile%.*}.jpg") -gt $1 ]]; then baperr "fatal: error compressing image"; rm "/tmp/bash-atproto/$workfile" "/tmp/bash-atproto/new-${workfile%.*}.jpg"; return 1; fi 224 + rm "/tmp/bash-atproto/$workfile" 225 + mv -f "/tmp/bash-atproto/new-${workfile%.*}.jpg" "/tmp/bash-atproto/${workfile%.*}.jpg" 217 226 workfile=${workfile%.*}.jpg 218 227 } 219 228 ··· 222 231 if [ -z "$4" ]; then baperr "fatal: not enough parameters"; return 1; fi 223 232 if [ ! -f "$1" ]; then baperr "fatal: image not found"; return 1; fi 224 233 mkdir /tmp/bash-atproto 2>/dev/null 234 + local workfile mimetemp 225 235 workfile=$(uuidgen)."${1##*.}" 226 - cp "$1" /tmp/bash-atproto/$workfile 227 - exiftool -all= /tmp/bash-atproto/$workfile -overwrite_original 228 - if ! [ "$?" = "0" ]; then baperr "fatal: exiftool failed!"; rm /tmp/bash-atproto/$workfile 2>/dev/null; return 1; fi 229 - if [[ $(identify -format '%w' /tmp/bash-atproto/$workfile) -gt $3 ]] || [[ $(identify -format '%h' /tmp/bash-atproto/$workfile) -gt $4 ]]; then 230 - bapInternal_resizeImage $3 $4 231 - if ! [ "$?" = "0" ]; then return 1; fi 236 + mimetemp=$(file --mime-type -b "$bap_preparedImage") || return 1 237 + cp "$1" "/tmp/bash-atproto/$workfile" 238 + exiftool -all= "/tmp/bash-atproto/$workfile" -overwrite_original || { baperr "fatal: exiftool failed!"; rm "/tmp/bash-atproto/$workfile" 2>/dev/null; return 1; } 239 + if [[ $(exiftool -ImageWidth -s3 "/tmp/bash-atproto/$workfile") -gt $3 ]] || [[ $(exiftool -ImageHeight -s3 "/tmp/bash-atproto/$workfile") -gt $4 ]]; then 240 + bapInternal_resizeImage "$3" "$4" || return 1 232 241 fi 233 - if [[ $(stat -c %s /tmp/bash-atproto/$workfile) -gt $2 ]]; then 234 - bapInternal_compressImage $2 235 - if ! [ "$?" = "0" ]; then return 1; fi 242 + if [[ $(stat -c %s "/tmp/bash-atproto/$workfile") -gt $2 ]]; then 243 + bapInternal_compressImage "$2" || return 1 236 244 fi 237 245 bapecho "image preparation successful" 238 246 bap_preparedImage=/tmp/bash-atproto/$workfile 239 - bap_preparedMime=$(file --mime-type -b $bap_preparedImage) 240 - bap_preparedSize=$(stat -c %s $bap_preparedImage) 241 - bap_imageWidth=$(identify -format '%w' $bap_preparedImage) 242 - bap_imageHeight=$(identify -format '%h' $bap_preparedImage) 247 + bap_preparedMime=$(file --mime-type -b "$bap_preparedImage") 248 + bap_preparedSize=$(stat -c %s "$bap_preparedImage") 249 + bap_imageWidth=$(exiftool -ImageWidth -s3 "$bap_preparedImage") 250 + bap_imageHeight=$(exiftool -ImageHeight -s3 "$bap_preparedImage") 243 251 return 0 244 252 } 245 253 ··· 250 258 if [ -z "$2" ]; then baperr "fatal: Required argument missing"; return 1; fi 251 259 bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -X POST -H "Authorization: Bearer $savedAccess" -H "Content-Type: $2" --data-binary @"$1" "$savedPDS/xrpc/com.atproto.repo.uploadBlob") 252 260 bapInternal_errorCheck $? bap_postBlobToPDS "error: blob upload failed" || return $? 253 - bap_postedBlob=$(echo $bap_result | jq -r .blob.ref.'"$link"') 254 - bap_postedMime=$(echo $bap_result | jq -r .blob.mimeType) 255 - bap_postedSize=$(echo $bap_result | jq -r .blob.size) 261 + bap_postedBlob=$(echo "$bap_result" | jq -r .blob.ref.'"$link"') 262 + bap_postedMime=$(echo "$bap_result" | jq -r .blob.mimeType) 263 + bap_postedSize=$(echo "$bap_result" | jq -r .blob.size) 256 264 bapecho "Blob uploaded ($bap_postedBlob)" 257 265 return 0 258 266 } 259 267 260 - function bap_resolvePDS () { 268 + function bap_resolveDID () { 261 269 if [ -z "$1" ]; then baperr "fatal: no did specified"; return 1; fi 262 270 bapInternal_validateDID "$1" || return 1 263 - case "$(echo $1 | cut -d ':' -f 2)" in 271 + case "$(echo "$1" | cut -d ':' -f 2)" in 264 272 265 273 "plc") 266 274 bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" "$bap_plcDirectory/$1") ··· 268 276 ;; 269 277 270 278 "web") 271 - bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" "$(echo https://$1 | sed 's/did:web://g')/.well-known/did.json") 279 + bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" "$(echo "https://$1" | sed 's/did:web://g')/.well-known/did.json") 272 280 bapInternal_errorCheck $? bap_resolvePDS "fatal: did:web lookup failed" || return $? 273 281 ;; 274 282 275 283 *) 276 - baperr "fatal: unrecognized did type" 284 + baperr "fatal: unknown did method $(echo "$1" | cut -d ':' -f 2)" 277 285 return 1 278 286 ;; 279 287 esac 280 - bap_resolve=$(echo $bap_result | jq -re .service) 281 - if ! [ "$?" = "0" ]; then baperr "fatal: failed to parse DID document"; return 1; fi 288 + echo "$bap_result" 289 + return 0 290 + } 291 + 292 + function bap_resolvePDS () { 293 + bap_resolve=$(bap_resolveDID "$1" | jq -re .service) || { baperr "fatal: failed to parse DID document"; return 1; } 282 294 iter=0 283 295 while read -r id; do 284 296 if ! [ "$id" = "#atproto_pds" ]; then ··· 291 303 return 0 292 304 } 293 305 294 - function bap_findPDS () { 295 - savedPDS=$(bap_resolvePDS $1) 296 - if [ -z "$savedPDS" ]; then baperr "fatal: PDS not found in DID document"; return 1; fi 306 + function bap_resolveHandle () { 307 + if ! bapInternal_validateDID "$1" 2> /dev/null; then baperr "fatal: specify did as first parameter"; return 1; fi 308 + local bap_temp bap_temp2 309 + bap_temp=$(bap_resolveDID "$1") || return $? 310 + bap_temp2=$(echo "$bap_temp" | jq -re ".alsoKnownAs.[0]" | cut -d '/' -f 3) 311 + if ! bapInternal_validateHandle "$bap_temp2" 2> /dev/null; then baperr "fatal: did doc returned invalid handle: $bap_temp2"; return 1; fi 312 + echo "$bap_temp2" 297 313 return 0 298 314 } 299 315 300 - function bap_resolveDID () { 316 + function bap_getDID () { 301 317 if [ -z "$1" ]; then baperr "specify identifier as first parameter"; return 1; fi 302 - if bapInternal_validateDID $1 2> /dev/null; then 303 - echo $1 318 + if bapInternal_validateDID "$1" 2> /dev/null; then 319 + if [ "$bap_disableOptionalChecks" != "1" ]; then bap_resolveDID "$1" > /dev/null || return $?; fi 320 + echo "$1" 304 321 return 0 305 322 306 - elif [[ "$1" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]; then 307 - baperrverb "Looking up handle from $bap_handleResolveURL" 308 - bap_temp=$(curl -s -A "$bap_curlUserAgent" -G --data-urlencode "handle=$1" "$bap_handleResolveURL/xrpc/com.atproto.identity.resolveHandle" | jq -re .did) 309 - if [ "$?" != "0" ]; then 310 - baperr "Error obtaining DID from API" 311 - return 2 323 + elif bapInternal_validateHandle "$1" 2> /dev/null; then 324 + baperrverb "Looking up handle from DNS" 325 + local bap_temp 326 + bap_temp=$(dig -t txt _atproto."$1" +short | cut -d '=' -f 2 | tr -d '"') 327 + if ! bapInternal_validateDID "$bap_temp" 2>/dev/null; then 328 + baperrverb "Looking up handle from website" 329 + bap_temp=$(curl -s -A "$bap_curlUserAgent" "https://$1/.well-known/atproto-did") 330 + if [ "$?" != "0" ] || ! bapInternal_validateDID "$bap_temp" 2>/dev/null; then 331 + baperr "Error resolving handle" 332 + return 2 333 + fi 312 334 fi 313 - echo $bap_temp 335 + echo "$bap_temp" 314 336 return 0 315 - 316 337 else 317 338 baperr "fatal: input not a handle or did" 318 339 return 1 319 - 320 340 fi 321 - # should not get here 322 - return 1 323 341 } 324 342 325 343 function bap_didInit () { 326 - savedDID=$(bap_resolveDID "$1") || return $? 344 + savedDID=$(bap_getDID "$1") || return $? 327 345 bapecho "Using DID: $savedDID" 328 346 return 0 329 347 } 330 348 331 349 function bap_getRecord () { 350 + local bap_result bap_temp bap_temp2 332 351 # get did of user 333 352 if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "bash-atproto: fetching did..."; fi 334 - bap_temp[0]=$(bap_resolveDID $(echo $1 | cut -d '/' -f 3)) || { baperr "failed to fetch did of record creator"; return 1; } 353 + bap_temp=$(bap_getDID "$(echo "$1" | cut -d '/' -f 3)") || { baperr "failed to fetch did of record creator"; return 1; } 335 354 # get their pds 336 355 if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "pds..."; fi 337 - bap_temp[1]=$(bap_resolvePDS ${bap_temp[0]}) || { baperr "failed to fetch pds of record creator"; return 1; } 356 + bap_temp2=$(bap_resolvePDS "$bap_temp") || { baperr "failed to fetch pds of record creator"; return 1; } 338 357 # get the post 339 - if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "post..."; fi 340 - bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" -G --data-urlencode "repo=${bap_temp[0]}" --data-urlencode "collection=$(echo $1 | cut -d '/' -f 4)" --data-urlencode "rkey=$(echo $1 | cut -d '/' -f 5)" ${bap_temp[1]}/xrpc/com.atproto.repo.getRecord) || { bapInternal_errorCheck $? bap_getRecord "failed to fetch record"; return 1; } 341 - echo $bap_result 342 - baperrverb "ok" 343 - bap_result= 358 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "record..."; fi 359 + bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" -G --data-urlencode "repo=$bap_temp" --data-urlencode "collection=$(echo "$1" | cut -d '/' -f 4)" --data-urlencode "rkey=$(echo "$1" | cut -d '/' -f 5)" "$bap_temp2/xrpc/com.atproto.repo.getRecord") || { bapInternal_errorCheck $? bap_getRecord "failed to fetch record"; return 1; } 360 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo "ok"; fi 361 + echo "$bap_result" 344 362 return 0 345 363 } 346 364 347 365 function bap_getServiceAuth () { 348 366 # Service, Lifetime, Lexicon 349 367 if [ -z "$1" ]; then baperr "Required argument missing"; return 2; fi 350 - if ! bapInternal_validateDID $1 2> /dev/null; then baperr "input must be a DID"; return 1; fi 368 + if ! bapInternal_validateDID "$1" 2> /dev/null; then baperr "input must be a DID"; return 1; fi 351 369 bap_temp="aud=$1" 352 370 if [ -n "$2" ]; then bap_temp="$bap_temp&exp=$(($2 + $(date +%s)))"; fi 353 371 if [ -n "$3" ]; then bap_temp="$bap_temp&lxm=$3"; fi 354 372 bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" -G -H "Authorization: Bearer $savedAccess" "$savedPDS/xrpc/com.atproto.server.getServiceAuth?$bap_temp") 355 373 bapInternal_errorCheck $? bap_getServiceAuth "fatal: failed to mint service auth token" || return $? 356 - echo $bap_result | jq -r .token 374 + echo "$bap_result" | jq -r .token 357 375 # PDS may change the expiry that bap requests 358 376 if [ -n "$2" ]; then 359 - bap_decodeJwt "$(echo $bap_result | jq -r .token)" 360 - if [ "$(($2 + $(date +%s)))" != "$(echo $bap_jwt | jq -r .exp)" ]; then baperr "warn: expiry time mismatch: got $(echo $bap_jwt | jq -r .exp), expected $(($2 + $(date +%s)))"; fi 377 + # local bap_jwt ? 378 + bap_decodeJwt "$(echo "$bap_result" | jq -r .token)" 379 + if [ "$(($2 + $(date +%s)))" != "$(echo "$bap_jwt" | jq -r .exp)" ]; then baperr "warn: expiry time mismatch: got $(echo "$bap_jwt" | jq -r .exp), expected $(($2 + $(date +%s)))"; fi 361 380 fi 362 381 bap_result= 363 382 return 0 364 383 } 384 + 385 + function bap_generateDatetime () { 386 + local bap_temp 387 + bap_temp=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 388 + # if date doesn't support sub-second precision, just fake it lol 389 + if ! [[ "$(echo "$bap_temp" | cut -d '.' -f 2 | cut -c -3)" =~ ^[0-9]+$ ]]; then bap_temp=$(date -u +%Y-%m-%dT%H:%M:%S."$(echo 00$((RANDOM % 999)) | grep -o '...$')"Z); fi 390 + if ! [[ $bap_temp =~ ^[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-6][0-9]:[0-6][0-9](.[0-9]{1,20})?(Z|([+-][0-2][0-9]:[0-5][0-9]))$ ]]; then baperr "generated datetime is invalid"; return 1; fi 391 + echo "$bap_temp" 392 + return 0 393 + }
+11 -3
docs/HOWTO.md
··· 7 7 ## Log in 8 8 9 9 1. `source bash-atproto.sh` 10 - 2. `bap_didInit <your handle or did>` 11 - 3. `bap_findPDS $savedDID` 12 - 4. `bap_getKeys $savedDID <app password>` 10 + 2. `bap_getKeys <did or handle> <app password> <your PDS, optional>` 13 11 14 12 The access and refresh tokens are stored in memory. You can write them to disk with `bap_saveSecrets <file name>` and load them with `bap_loadSecrets`. After obtaining the tokens, information about the account is fetched from the access token and loading from the tokens won't require any further account information. 15 13 ··· 87 85 You can also set the post in progress to be a quote post with `bapBsky_cyorAddQuote`. It takes an AT URI as input just like replies. 88 86 89 87 Note: Quote posts with media have a different structure for the media in question. The native `bapBsky_cyorAddImage` and `bapBsky_cyorAddVideo` functions handle this transparently, but you'll need to keep that in mind if you manually edit the JSON. 88 + 89 + ### Embeds 90 + 91 + Link embeds are great, and you can add one with `bapBsky_cyorAddExternalEmbed`. There are two ways to use this function, depending on whether you want a thumbnail or not: 92 + 93 + 1. Link and text only: `bapBsky_cyorAddExternalEmbed <URL> <title> <description>` 94 + 95 + 2. With a thumbnail: `bapBsky_cyorAddExternalEmbed <URL> <title> <description> $bap_postedBlob $bap_postedMime $bap_postedSize` 96 + 97 + Note that if you're using a thumbnail, you need to mind the dimensions that the client app expects. For example, most link thumbnails should be 2000x1050, and YouTube embeds are 2000x1500, have black bars on the top and bottom, and are cropped by the client. 90 98 91 99 ### Adding (hidden hash)tags 92 100