atproto library for bash scripts
atproto bash-atproto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

bash-atproto v4 (massive changelog below!)

- bap_resolveDID was renamed to bap_getDID. It also no longer calls to a resolve server in favor of resolving the handle itself. (DNS and HTTPS are performed in that order)
- bap_getKeys no longer uses $savedPDS. It instead opts to resolve the PDS from the specified identifier, or from the third parameter (new!).
- bap_getKeys and bap_refreshKeys now load data from the obtained JWT after a successful login/refresh. It is unnecessary to manually manage $savedDID now. $savedPDS is only needed when working with an account on a PDS that isn't it's "blessed" one.
- bap_findPDS has been removed as it is unnecessary with the bap_getKeys changes.
- bap_postToBluesky was removed. Use bapBsky_createPost with bap-bsky.sh instead.
- $bap_dryRun variable was added. Set it to 1 to see all records that would normally be posted.
- bap_resolveDID (the new one) was split from bap_resolvePDS, and it prints the DID doc.
- The check to see if the account is active in bap_getKeys and bap_refreshKeys can be skipped by setting $bap_disableOptionalChecks to 1.
- bap_generateDatetime to get an atproto datetime of the current date and time
- bap_postRecord can set rkey and whether to validate the record against the lexicon
- bap_getDID now verifies the DID exists if it was manually specified
- bap_prepareImage now uses exiftool instead of imagemagick for calculating dimensions. It should be able to process videos under certain conditions.
- bash-atproto now depends on dig.
- Fixed issue with generating datetimes on non-GNU systems (Closes #5)

bap-bsky v2 changes:
- Removed bapBsky_cyorAddLabel as it has been replaced by bapBsky_cyorAddSelfLabels
- bapBsky_prepareVideo was renamed to bapBsky_prepareVideoIndirect, now only accepts mp4 videos, and scrubs metadata
- Removed compatibility stubs
- CYOR commands that fetch posts now require login. They also now respect threadgates and won't work if there's a block relationship.
- bapBsky_cyorGetReplyRoot was renamed to bapBskyInternal_cyorGetReplyRoot and now always runs after bapBsky_cyorAddReply
- bap-bsky now calls the AppView API set in bapBsky_bskyAppViewDID (default Bluesky) for some requests
- bapBsky_getPost fetches a Bluesky post with auth
- bapBsky_getProfile fetches a Bluesky profile with auth
- bapBsky_verifyActor creates a verification record
- bapBsky_followActor follows an actor or ensures an actor is being followed
- bapBsky_likePost likes a post or ensures it has been liked (does not use via yet)
- bapBsky_submitPost now passes through arguments for the bap_postRecord changes
- createdAt is no longer added automatically if already present in the record
- Now uses bap_generateDatetime

bap-extra v2 changes:
- New bapExt_bskyCancelActor which creates an app.bsky.graph.cancellation record (see cred.blue/canceler)
- Removed bapExt_fastLogin as bap_getKeys acts like it now
- bapExt_checkDeps now checks for dig

bap-sprk v2.1 changes:
- Now uses bap_generateDatetime
- bapSprk_submitPost now passes through arguments for the bap_postRecord changes
- bapSprk_cyorAddReply can make the post being worked on a reply

Engielolz a8acdc7d a0eab9de

+307 -188
+16 -12
README.md
··· 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
+137 -90
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: $*" ··· 15 16 function bapBskyEcho () { 16 17 if [ ! "$bap_verbosity" -ge 1 ]; then return 0; fi 17 18 echo "bap-bsky: $*" 19 + } 20 + 21 + function bapBskyErrVerb () { 22 + if [ ! "$bap_verbosity" -ge 2 ]; then return 0; fi 23 + >&2 echo "bap-bsky: $*" 18 24 } 19 25 20 26 function bapBsky_cyorInit () { ··· 65 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 86 bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .reply.root ··· 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 $? 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 96 97 bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .reply.parent 97 98 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 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");; ··· 177 180 } 178 181 179 182 function bapBsky_submitPost () { 180 - bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 181 - bap_postRecord "$bap_cyorRecord" || return $? 183 + if ! echo $bap_cyorRecord | jq -re .createdAt > /dev/null; then bapCYOR_str createdAt $(bap_generateDatetime); fi 184 + bapBskyErrVerb "submitting record..." 185 + bap_postRecord "$bap_cyorRecord" "$1" "$2" || return $? 182 186 bap_cyorRecord= 183 187 uri=$(echo $bap_result | jq -r .uri) 184 188 cid=$(echo $bap_result | jq -r .cid) ··· 195 199 return 0 196 200 } 197 201 198 - 199 202 function bapBsky_createRepost () { # arguments 1 is uri, 2 is cid. error codes same as postToBluesky 200 203 if [ -z "$2" ]; then bapBskyErr "fatal: Required argument missing"; return 1; fi 204 + if [ "$bapBsky_skipRepostChecks" != "1" ]; then 205 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 206 + if [ "$(echo $bapBsky_temp | jq -r .viewer.repost)" != "null" ]; then bapBskyErr "you already reposted this post!"; return 0; fi 207 + fi 201 208 bap_cyorRecord= 202 209 bapCYOR_str \$type app.bsky.feed.repost 203 210 bapCYOR_str cid $2 .subject 204 211 bapCYOR_str uri $1 .subject 212 + if [ "$3" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 205 213 bapBsky_submitPost || return $? 206 214 bapBskyEcho "Repost record at $uri" 207 215 return 0 ··· 238 246 } 239 247 240 248 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 249 # $1 is file 248 250 # $2 is mime (like bap_postBlobToPDS) 249 251 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 252 + if [ "$2" != "video/mp4" ]; then bapBskyErr "videos must be mp4"; fi 251 253 bapBsky_checkVideo "$1" || return $? 254 + # bap_prepareImage "$1" 100000000 2147483647 2147483647 252 255 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 256 + exiftool -all= "$1" -o "/tmp/$uuid.mp4" || return $? 258 257 bap_postBlobToPDS "/tmp/$uuid.mp4" "$2" || { bapBskyErr "warning: video upload failed"; rm "/tmp/$uuid.mp4"; return 1; } 259 258 rm "/tmp/$uuid.mp4" 260 259 bap_imageWidth=$(exiftool -ImageWidth -s3 $1) 261 260 bap_imageHeight=$(exiftool -ImageHeight -s3 $1) 262 - bapBskyEcho 'video "posted"' 261 + bapBskyEcho 'uploaded video' 263 262 return 0 264 263 } 265 264 266 265 function bapBsky_getVideoLimits () { 267 266 # TODO: figure out atproto-proxy for Lumi 268 267 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 $? 268 + bapBsky_temp=$(bap_getServiceAuth did:web:$bapBsky_lumiURL "" app.bsky.video.getUploadLimits) || return $? 269 + bap_result=$(curl --fail-with-body -s -A "$bap_curlUserAgent" -H "Authorization: Bearer $bapBsky_temp" "https://$bapBsky_lumiURL/xrpc/app.bsky.video.getUploadLimits") 270 + bapInternal_errorCheck $? bapBsky_getvideoLimits "fatal: failed to get video limits" || return $? 272 271 if [ "$1" = "--raw" ]; then echo $bap_result; else 273 272 if [ "$(echo $bap_result | jq -r .canUpload)" != "true" ]; then echo $bap_result | jq -r .message; return 1; fi 274 273 echo $bap_result | jq -r .message ··· 277 276 fi 278 277 } 279 278 279 + # function bapBsky_videoUpload () { 280 + # local bap_resultcode 281 + # 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") 282 + # bap_resultcode=$? 283 + # # BskyVideo can throw HTTP 409 if it's already done, check for that 284 + # if [ "$(echo $bap_result | jq -r .jobStatus.error)" = "already_exists" ]; then bap_resultcode=0; fi 285 + # bapInternal_errorCheck $bap_resultcode bapBsky_videoUpload "fatal: failed to upload video" || return $? 286 + # # give job id stuff 287 + # bapBsky_jobId=$(echo "$bap_result" | jq -r .jobStatus.jobId) 288 + # return 0 289 + # } 290 + # 291 + # function bapBsky_videoStatus () { 292 + # # no auth needed? 293 + # curl --fail-with-body -s -A "$bap_curlUserAgent" "https://$bapBsky_lumiURL/xrpc/app.bsky.video.getJobStatus?jobId=$1" 294 + # } 295 + 296 + function bapBsky_prepareVideo () { 297 + # if [ -z "$2" ]; then bapBskyErr "fatal: Required argument missing"; return 1; fi 298 + # bapBsky_checkVideo "$1" || return $? 299 + # local bapBsky_temp 300 + # #bapBsky_temp=$(bap_getServiceAuth did:web:video.bsky.app 900 com.atproto.server.uploadBlob) || return $? 301 + # bapBsky_temp=$(bap_getServiceAuth did:web:$(echo $savedPDS | sed 's|https://||g') 900 com.atproto.server.uploadBlob) || return $? 302 + # bapBsky_videoUpload "$bapBsky_temp" "$1" "$2" || return $? 303 + # while :; do 304 + # bapBsky_temp=$(bapBsky_videoStatus "$bapBsky_jobId" | jq -r '.jobStatus.state') || { bapBskyErr "unexpected response from videoStatus"; return 1; } 305 + # if [ "$bapBsky_temp" = "JOB_STATUS_COMPLETED" ]; then break; fi 306 + # if [ "$bapBsky_temp" = "JOB_STATUS_FAILED" ]; then bapBskyErr "the video failed to process"; return 1; fi 307 + # sleep 1 308 + # done 309 + # bap_postedBlob=$(echo $bapBsky_temp | jq -r .jobStatus.blob) 310 + # bap_postedMime="video/mp4" 311 + # bap_imageWidth=$(exiftool -ImageWidth -s3 $1) 312 + # bap_imageHeight=$(exiftool -ImageHeight -s3 $1) 313 + # return 0 314 + bapBskyErr "function not implemented. try using bapBsky_prepareVideoIndirect" 315 + return 1 316 + } 317 + 280 318 function bapBsky_postVideo () { 281 319 # param: 282 320 # 1-5 - see bapBsky_cyorAddVideo ··· 290 328 return 0 291 329 } 292 330 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 331 + function bapBsky_verifyActor () { 332 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 333 + local bap_cyorRecord bapBsky_temp bapBsky_temp2 334 + bapBsky_temp2=$(bap_getDID "$1") || return $? 335 + bapBsky_temp=$(bap_getRecord at://$bapBsky_temp2/app.bsky.actor.profile/self) || { bapBskyErr "failed to resolve profile for verification"; return 1; } 336 + bap_cyorRecord= 337 + bapCYOR_str \$type "app.bsky.graph.verification" 338 + bapCYOR_str handle "$(bap_resolveHandle "$bapBsky_temp2")" 339 + bapCYOR_str subject "$bapBsky_temp2" 340 + bapCYOR_str displayName "$(echo $bapBsky_temp | jq -r .value.displayName)" 341 + if [ "$2" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 342 + bapBsky_submitPost || return $? 343 + bapBskyEcho "verified $(echo $bapBsky_temp | jq -r .value.displayName)" 344 + return 0 300 345 } 301 346 302 - function bapCYOR_bskypost () { 303 - bapBsky_stubWarn bapBsky_cyorInit || return $? 304 - bapBsky_cyorInit 305 - return $? 347 + function bapBsky_getProfile () { 348 + if [ -z "$savedAccess" ]; then bapBskyErr "needs auth!"; return 1; fi 349 + if ! bapInternal_validateDID "$1" 2>/dev/null && ! bapInternal_validateHandle "$1" 2>/dev/null; then bapBskyErr "bad identifier"; return 1; fi 350 + local bap_result 351 + 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") 352 + bapInternal_errorCheck $? bapBsky_getProfile "failed to get profile for $1" || return $? 353 + echo "$bap_result" 354 + return 0 306 355 } 307 356 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 $? 319 - } 320 - 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 $? 331 - } 332 - 333 - function bap_checkVideoForBluesky () { 334 - bapBsky_stubWarn bapBsky_checkVideo || return $? 335 - bapBsky_checkVideo "$@" 336 - return $? 357 + function bapBsky_followActor () { 358 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 359 + local bap_cyorRecord= bapBsky_temp 360 + bapBsky_temp=$(bapBsky_getProfile "$1") || return $? 361 + 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 362 + 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 363 + 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 364 + if [ "$(echo $bapBsky_temp | jq -r .viewer.following)" != "null" ]; then bapBskyErr "already following $(echo $bapBsky_temp | jq -r .displayName)"; return 0; fi 365 + if [ "$(echo $bapBsky_temp | jq -r .did)" = "$savedDID" ]; then bapBskyErr "you can't follow yourself!"; return 1; fi 366 + bapCYOR_str \$type "app.bsky.graph.follow" 367 + bapCYOR_str subject "$(bap_getDID "$1" || return 1)" 368 + if [ "$2" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 369 + bapBsky_submitPost || return $? 370 + bapBskyEcho "now following $(echo $bapBsky_temp | jq -r .displayName)" 371 + return 0 337 372 } 338 373 339 - function bap_prepareVideoForBluesky () { 340 - bapBsky_stubWarn bapBsky_prepareVideo || return $? 341 - bapBsky_prepareVideo "$@" 342 - return $? 374 + function bapBsky_getPost () { 375 + if [ -z "$savedAccess" ]; then bapBskyErr "needs auth!"; return 1; fi 376 + if [ "$(echo $1 | cut -d '/' -f 4)" != "app.bsky.feed.post" ]; then bapBskyErr "this is not a post!"; return 1; fi 377 + local bap_result 378 + bapBskyErrVerb "looking up post..." 379 + 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)") 380 + bapInternal_errorCheck $? bapBsky_getPost "failed to get post with uri $1" || return $? 381 + echo "$bap_result" | jq -r .posts.[0] 382 + 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 383 + return 0 343 384 } 344 385 345 - function bap_postVideoToBluesky () { 346 - bapBsky_stubWarn bapBsky_postVideo || return $? 347 - bapBsky_postVideo "$@" 348 - return $? 386 + function bapBsky_likePost () { 387 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 388 + local bap_cyorRecord= bapBsky_temp 389 + bapBsky_temp=$(bapBsky_getPost "$1" -e) || return $? 390 + if [ "$(echo $bapBsky_temp | jq -r .viewer.like)" != "null" ]; then bapBskyErr "you already liked this post!"; return 0; fi 391 + bapCYOR_str \$type "app.bsky.feed.like" 392 + bapCYOR_str cid "$(echo $bapBsky_temp | jq -r .cid)" .subject 393 + bapCYOR_str uri "$(echo $bapBsky_temp | jq -r .uri)" .subject 394 + if [ "$2" = "--no-create" ]; then declare -g bap_cyorRecord=$bap_cyorRecord; return 0; fi 395 + bapBsky_submitPost || return $? 349 396 }
+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 + }
+28 -4
bap-sprk.sh
··· 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 6 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 7 + if [ "$bap_internalVersion" != "4" ] || ! [ "$bap_internalMinorVer" -ge "-1" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 8 8 9 9 10 10 bapSprk_internalVersion=2 11 - bapSprk_internalMinorVer=0 11 + bapSprk_internalMinorVer=1 12 12 13 13 function bapSprk_err () { 14 14 >&2 echo "bap-sprk: $*" ··· 51 51 } 52 52 53 53 function bapSprk_submitPost () { 54 - bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 55 - bap_postRecord "$bap_cyorRecord" || return $? 54 + bapCYOR_str createdAt $(bap_generateDatetime) 55 + bap_postRecord "$bap_cyorRecord" "$1" "$2" || return $? 56 56 bap_cyorRecord= 57 57 uri=$(echo $bap_result | jq -r .uri) 58 58 cid=$(echo $bap_result | jq -r .cid) ··· 100 100 bapSprk_echo "Posted record at $uri" 101 101 return 0 102 102 } 103 + 104 + function bapSprkInternal_cyorGetReplyRoot () { 105 + if [ -z "$1" ]; then bapSprkErr "error: Required argument missing"; return 1; fi 106 + bapSprk_temp=$(bap_getRecord $1) || return $? 107 + if echo $bapSprk_temp | jq -re '.value.reply.root.uri' > /dev/null; then 108 + # copy values 109 + bapCYOR_str uri $(echo $bapSprk_temp | jq -re '.value.reply.root.uri') .reply.root 110 + bapCYOR_str cid $(echo $bapSprk_temp | jq -re '.value.reply.root.cid') .reply.root 111 + else 112 + # we just wasted time and bandwidth! 113 + bapCYOR_str uri $(echo $bapSprk_temp | jq -re '.uri') .reply.root 114 + bapCYOR_str cid $(echo $bapSprk_temp | jq -re '.cid') .reply.root 115 + fi 116 + return 0 117 + } 118 + 119 + function bapSprk_cyorAddReply () { 120 + if [ -z "$1" ]; then bapSprkErr "error: Required argument missing"; return 1; fi 121 + bapSprk_temp=$(bap_getRecord $1) || return $? 122 + bapCYOR_str uri $(echo $bapSprk_temp | jq -re '.uri') .reply.parent 123 + bapCYOR_str cid $(echo $bapSprk_temp | jq -re '.cid') .reply.parent 124 + bapSprkInternal_cyorGetReplyRoot $1 125 + return 0 126 + }
+85 -57
bash-atproto.sh
··· 1 1 #!/bin/bash 2 2 # SPDX-License-Identifier: MIT 3 - bap_internalVersion=3 4 - bap_internalMinorVer=3 3 + bap_internalVersion=4 4 + bap_internalMinorVer=-1 5 5 6 6 # you can change these 7 7 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)" 8 + 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 2>/dev/null)" 10 9 bap_chmodSecrets=1 11 10 bap_verbosity=1 11 + bap_disableOptionalChecks=0 12 12 13 13 function baperr () { 14 14 >&2 echo "bash-atproto: $*" ··· 44 44 savedAccessExpiry="$(echo $bap_jwt | jq -r .exp)" 45 45 } 46 46 47 + function bapInternal_verifyStatus () { 48 + if [ "$bap_disableOptionalChecks" = "1" ]; then return 0; fi 49 + if [ "$(echo $bap_result | jq -r .active)" = "false" ]; then 50 + baperr "warning: account is inactive" 51 + 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 52 + return 115 53 + fi 54 + } 55 + 47 56 function bap_loadSecrets () { 48 57 if [[ -f $1 ]]; then while IFS= read -r line; do declare -g "$line"; done < "$1" 49 58 bap_decodeJwt "$savedAccess" || return 1 ··· 86 95 esac 87 96 } 88 97 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 95 - } 96 - 97 98 function bapInternal_validateDID () { 98 99 if ! [[ "$1" =~ ^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$ ]]; then baperr "fatal: input not a did"; return 1; fi 99 100 return 0 100 101 } 101 102 103 + function bapInternal_validateHandle () { 104 + 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 105 + return 0 106 + } 107 + 102 108 function bap_getKeys () { # 1: failure 2: user error 103 109 if [ -z "$2" ]; then baperr "No app password was passed"; return 2; fi 110 + local lSavedPDS=$3 111 + if [ -z "$lSavedPDS" ]; then lSavedPDS=$(bap_resolvePDS $(bap_getDID "$1")) || return $?; fi 104 112 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") 113 + 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 114 bapInternal_errorCheck $? bap_getKeys "fatal: failed to authenticate" || return $? 107 115 bapecho secured the keys! 108 116 savedAccess=$(echo $bap_result | jq -r .accessJwt) ··· 110 118 # we don't care about the handle 111 119 bap_decodeJwt $savedAccess 112 120 if [ "$(echo $bap_jwt | jq -r .scope)" = "com.atproto.access" ]; then baperr "warning: this is not an app password"; fi 121 + bapInternal_loadFromJwt 113 122 bapInternal_verifyStatus || return $? 114 123 return 0 115 124 } ··· 122 131 savedAccess=$(echo $bap_result | jq -r .accessJwt) 123 132 savedRefresh=$(echo $bap_result | jq -r .refreshJwt) 124 133 bap_decodeJwt $savedAccess 134 + bapInternal_loadFromJwt 125 135 bapInternal_verifyStatus || return $? 126 136 return 0 127 137 } ··· 176 186 177 187 function bapInternal_finalizeRecord () { 178 188 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}" 189 + local bap_temp 190 + if [ "$2" = "true" ]; then bap_temp=", \"validate\": true"; fi 191 + if [ "$2" = "false" ]; then bap_temp=", \"validate\": false"; fi 192 + if [ -n "$3" ]; then bap_temp="$bap_temp, \"rkey\": \"$3\""; fi 193 + bap_finalRecord="{\"collection\": $(echo $1 | jq -c '.["$type"]'), \"repo\": \"$savedDID\", \"record\": $1$bap_temp}" 180 194 if ! jq -e . >/dev/null <<<"$1"; then baperr "finalize: JSON parse error"; return 1; fi 181 195 return 0 182 196 } 183 197 184 198 function bap_postRecord () { 185 - bapInternal_finalizeRecord "$1" || { baperr "not posting because finalize failed"; return 1; } 199 + bapInternal_finalizeRecord "$1" "$2" "$3"|| { baperr "not posting because finalize failed"; return 1; } 200 + if [ "$bap_dryRun" = "1" ]; then 201 + bapecho "The following $(echo "$bap_finalRecord" | jq -r '.record.["$type"]') record would be sent to the PDS:" 202 + echo "$bap_finalRecord" | jq 203 + return 1 204 + fi 186 205 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 206 bapInternal_errorCheck $? bap_postRecord "failed to post record" || return $? 188 207 return 0 189 208 } 190 209 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 210 function bapInternal_resizeImage () { 211 + if ! [[ "$mimetemp" =~ ^image ]]; then baperr "can't resize non-pictures"; return 2; fi 205 212 bapecho "need to resize image" 206 213 convert /tmp/bash-atproto/$workfile -resize $1x$2 /tmp/bash-atproto/new-$workfile 207 214 if ! [ "$?" = "0" ]; then baperr "fatal: convert failed!"; rm /tmp/bash-atproto/$workfile 2>/dev/null; return 1; fi ··· 209 216 } 210 217 211 218 function bapInternal_compressImage () { 219 + if ! [[ "$mimetemp" =~ ^image ]]; then baperr "can't compress non-pictures"; return 2; fi 212 220 bapecho "image is too big, trying to compress" 213 221 convert /tmp/bash-atproto/$workfile -define jpeg:extent=$1 /tmp/bash-atproto/new-${workfile%.*}.jpg 214 222 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 ··· 222 230 if [ -z "$4" ]; then baperr "fatal: not enough parameters"; return 1; fi 223 231 if [ ! -f "$1" ]; then baperr "fatal: image not found"; return 1; fi 224 232 mkdir /tmp/bash-atproto 2>/dev/null 233 + local workfile mimetemp 225 234 workfile=$(uuidgen)."${1##*.}" 235 + mimetemp=$(file --mime-type -b $bap_preparedImage) || return 1 226 236 cp "$1" /tmp/bash-atproto/$workfile 227 237 exiftool -all= /tmp/bash-atproto/$workfile -overwrite_original 228 238 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 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 242 if [[ $(stat -c %s /tmp/bash-atproto/$workfile) -gt $2 ]]; then 234 - bapInternal_compressImage $2 235 - if ! [ "$?" = "0" ]; then return 1; fi 243 + bapInternal_compressImage $2 || return 1 236 244 fi 237 245 bapecho "image preparation successful" 238 246 bap_preparedImage=/tmp/bash-atproto/$workfile 239 247 bap_preparedMime=$(file --mime-type -b $bap_preparedImage) 240 248 bap_preparedSize=$(stat -c %s $bap_preparedImage) 241 - bap_imageWidth=$(identify -format '%w' $bap_preparedImage) 242 - bap_imageHeight=$(identify -format '%h' $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 ··· 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 271 case "$(echo $1 | cut -d ':' -f 2)" in ··· 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 318 if bapInternal_validateDID $1 2> /dev/null; then 319 + if [ "$bap_disableOptionalChecks" != "1" ]; then bap_resolveDID "$1" > /dev/null || return $?; fi 303 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 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 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[0]=$(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 356 bap_temp[1]=$(bap_resolvePDS ${bap_temp[0]}) || { 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 358 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "record..."; fi 340 359 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; } 360 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo "ok"; fi 341 361 echo $bap_result 342 - baperrverb "ok" 343 - bap_result= 344 362 return 0 345 363 } 346 364 ··· 362 380 bap_result= 363 381 return 0 364 382 } 383 + 384 + function bap_generateDatetime () { 385 + local bap_temp 386 + bap_temp=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 387 + # if date doesn't support sub-second precision, just fake it lol 388 + 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 389 + 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 390 + echo "$bap_temp" 391 + return 0 392 + }
+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