atproto library for bash scripts
atproto bash-atproto

bash-atproto v3.2, bap-bsky v1.1 and bap-sprk v2.0

bash-atproto: add bap_getRecord and bap_getServiceAuth
bash-atproto: fix plc directory resolve notice breaking bap_resolveDID
bash-atproto: bump version to 3.2
bap-bsky: Add bapBsky_cyorGetReplyRoot, bapBsky_cyorAddReply and bapBsky_cyorAddVideo
bap-bsky: bump version to 1.1
bap-sprk: move bapSprk_submitPost down so the cyor functions are at the top
bap-sprk: Add bapSprk_cyorAddVideo
bap-sprk: Add support for alt text in bapSprk_postVideo (BREAKING CHANGE)
bap-sprk: verify correct amount of parameters are passed to bapSprk_cyorAddImage
bap-sprk: bump version to 2.0
New bap-extra with bapExt_authWithATFileCreds

Engielolz 0554345d e432c93d

+51 -25
bap-bsky.sh
··· 1 1 #!/bin/bash 2 2 # SPDX-License-Identifier: MIT 3 3 # bap-bsky.sh: the bash-atproto functions pertaining to Bluesky Social. 4 - bapBsky_internalVersion=1 5 - bapBsky_internalMinorVer=0 6 - 7 4 if [ -z "$bap_internalVersion" ]; then >&2 echo "bash-atproto not loaded?"; return 127; fi 8 - if [ "$bap_internalVersion" != "3" ] || ! [ "$bap_internalMinorVer" -ge "0" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 5 + if [ "$bap_internalVersion" != "3" ] || ! [ "$bap_internalMinorVer" -ge "2" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 6 + 7 + bapBsky_internalVersion=1 8 + bapBsky_internalMinorVer=1 9 9 10 10 function bapBskyErr () { 11 11 >&2 echo "bap-bsky: $*" ··· 35 35 bapCYOR_add height $6 .embed.images.[$1].aspectRatio 36 36 } 37 37 38 - # # (porn, sexual, nudity), graphic-media 38 + function bapBsky_cyorAddVideo () { 39 + # param: 40 + # 1 - blob 41 + # 2 - size 42 + # 3 - width 43 + # 4 - height 44 + # 5 - alt text 45 + if [ -z "$4" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 46 + bapCYOR_str alt "$5" .embed 47 + bapCYOR_str \$type app.bsky.embed.video .embed 48 + bapCYOR_str \$type blob .embed.video 49 + bapCYOR_str \$link "$1" .embed.video.ref 50 + bapCYOR_str mimeType "video/mp4" .embed.video 51 + bapCYOR_add size $2 .embed.video 52 + bapCYOR_add width $3 .embed.aspectRatio 53 + bapCYOR_add height $4 .embed.aspectRatio 54 + } 55 + 56 + # (porn, sexual, nudity), graphic-media 39 57 function bapBsky_cyorAddLabel () { 40 58 if [ -z "$2" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 41 59 bapCYOR_str \$type com.atproto.label.defs#selfLabels .labels ··· 43 61 bapCYOR_str val $2 .labels.values.[$1] 44 62 } 45 63 64 + function bapBsky_cyorGetReplyRoot () { 65 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 66 + bapBsky_temp=$(bap_getRecord $1) || return $? 67 + if echo $bapBsky_temp | jq -re '.value.reply.root.uri' > /dev/null; then 68 + # copy values 69 + bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.value.reply.root.uri') .reply.root 70 + bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.value.reply.root.cid') .reply.root 71 + else 72 + # we just wasted time and bandwidth! 73 + bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .reply.root 74 + bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.cid') .reply.root 75 + fi 76 + return 0 77 + } 78 + 79 + function bapBsky_cyorAddReply () { 80 + if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi 81 + bapBsky_temp=$(bap_getRecord $1) || return $? 82 + bapCYOR_str uri $(echo $bapBsky_temp | jq -re '.uri') .reply.parent 83 + bapCYOR_str cid $(echo $bapBsky_temp | jq -re '.cid') .reply.parent 84 + if ! echo $bap_cyorRecord | jq -re '.reply.root' > /dev/null; then bapBsky_cyorGetReplyRoot $1 || return $?; fi 85 + return 0 86 + } 87 + 46 88 function bapBsky_submitPost () { 47 89 bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 48 90 bap_postRecord "$bap_cyorRecord" || return $? ··· 119 161 } 120 162 121 163 function bapBsky_postVideo () { 122 - # param: 123 - # 1 - blob 124 - # 2 - size 125 - # 3 - width 126 - # 4 - height 127 - # 5 - alt text 128 - # 6 - text 129 - # assuming video/mp4 is always the mimetype might be a bad assumption 164 + # param: 165 + # 1-5 - see bapBsky_cyorAddVideo 166 + # 6 - text 130 167 if [ -z "$4" ]; then bapBskyErr "fatal: more arguments required"; return 1; fi 131 168 bapBsky_cyorInit 132 169 bapCYOR_str text "$6" 133 - bapCYOR_str alt "$5" .embed 134 - bapCYOR_str \$type app.bsky.embed.video .embed 135 - bapCYOR_str \$type blob .embed.video 136 - bapCYOR_str \$link "$1" .embed.video.ref 137 - bapCYOR_str mimeType "video/mp4" .embed.video 138 - bapCYOR_add size $2 .embed.video 139 - bapCYOR_add width $3 .embed.aspectRatio 140 - bapCYOR_add height $4 .embed.aspectRatio 170 + bapBsky_cyorAddVideo $1 $2 $3 $4 "$5" 141 171 bapBsky_submitPost || return $? 142 172 bapBskyEcho "Posted record at $uri" 143 173 return 0 ··· 148 178 149 179 function bapBsky_stubWarn () { 150 180 bapBskyErr "warn: function ${FUNCNAME[1]} was renamed. call $1 instead" 151 - } 152 - 153 - function bap_fakeStub () { 154 - bapBsky_stubWarn bapBsky_lol 155 181 } 156 182 157 183 function bapCYOR_bskypost () {
+30
bap-extra.sh
··· 1 + #!/bin/bash 2 + # SPDX-License-Identifier: MIT 3 + # bap-extra.sh: Other functions that might be useful 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 6 + 7 + bapExt_internalVersion=1 8 + bapExt_internalMinorVer=0 9 + 10 + function bapExtErr () { 11 + >&2 echo "bap-extra: $*" 12 + } 13 + 14 + function bapExtEcho () { 15 + if [ ! "$bap_verbosity" -ge 1 ]; then return 0; fi 16 + echo "bap-extra: $*" 17 + } 18 + 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 + }
+28 -20
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 - bapSprk_internalVersion=1 7 - bapSprk_internalMinorVer=0 8 - 9 6 if [ -z "$bap_internalVersion" ]; then >&2 echo "bash-atproto not loaded?"; return 127; fi 10 7 if [ "$bap_internalVersion" != "3" ] || ! [ "$bap_internalMinorVer" -ge "0" ]; then >&2 echo "Incorrect bash-atproto version"; return 1; fi 8 + 9 + 10 + bapSprk_internalVersion=2 11 + bapSprk_internalMinorVer=0 11 12 12 13 function bapSprk_err () { 13 14 >&2 echo "bap-sprk: $*" ··· 24 25 bapCYOR_str text "" 25 26 } 26 27 27 - function bapSprk_submitPost () { 28 - bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 29 - bap_postRecord "$bap_cyorRecord" || return $? 30 - bap_cyorRecord= 31 - uri=$(echo $bap_result | jq -r .uri) 32 - cid=$(echo $bap_result | jq -r .cid) 33 - return 0 34 - } 35 - 36 28 function bapSprk_cyorAddImage () { 37 29 # Lexicon has alt text but no image dimensions 38 30 # 1 image, 2 blob, 3 mime, 4 size, 5 alt 31 + if [ -z "$4" ]; then bapSprkErr "error: Required argument missing"; return 1; fi 39 32 bapCYOR_str \$type so.sprk.embed.images .embed 40 33 bapCYOR_str alt "$5" .embed.images.[$1] 41 34 bapCYOR_str \$type so.sprk.embed.images#image .embed.images.[$1] ··· 47 40 #bapCYOR_add height $7 .embed.images.[$1].aspectRatio 48 41 } 49 42 43 + 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 51 + } 52 + 53 + function bapSprk_submitPost () { 54 + bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) 55 + bap_postRecord "$bap_cyorRecord" || return $? 56 + bap_cyorRecord= 57 + uri=$(echo $bap_result | jq -r .uri) 58 + cid=$(echo $bap_result | jq -r .cid) 59 + return 0 60 + } 61 + 50 62 function bapSprk_prepareImage () { 51 63 # 5MB image limit: at://sprk.so/app.bsky.feed.post/3lipdqef2k22n 52 64 # Get rid of dimensions liimt with really big numbers ··· 60 72 # 1 blob - $bap_postedImage 61 73 # 2 size - $bap_postedSize 62 74 # 3 mime - $bap_postedMime 75 + # 4 alt text 76 + # 5 post text 63 77 if [ -z "$3" ]; then bapSprk_err "fatal: more arguments required"; return 1; fi 64 78 bapSprk_cyorInit 65 - bapCYOR_str text "$4" 66 - bapCYOR_str \$type so.sprk.feed.post 67 - bapCYOR_str \$type so.sprk.embed.video .embed 68 - bapCYOR_str alt "" .embed 69 - bapCYOR_str \$type blob .embed.video 70 - bapCYOR_str \$link $1 .embed.video.ref 71 - bapCYOR_str mimeType "$bap_postedMime" .embed.video 72 - bapCYOR_add size $bap_postedSize .embed.video 79 + bapCYOR_str text "$5" 80 + bapSprk_cyorAddVideo $1 $2 $3 "$4" 73 81 bapSprk_submitPost "$bap_cyorRecord" || return $? 74 82 bapSprk_echo "Posted record at $uri" 75 83 return 0
+42 -2
bash-atproto.sh
··· 1 1 #!/bin/bash 2 2 # SPDX-License-Identifier: MIT 3 3 bap_internalVersion=3 4 - bap_internalMinorVer=1 4 + bap_internalMinorVer=2 5 5 6 6 # you can change these 7 7 bap_plcDirectory=https://plc.directory ··· 22 22 function bapverbose () { 23 23 if [ ! "$bap_verbosity" -ge 2 ]; then return 0; fi 24 24 echo "bash-atproto: $*" 25 + } 26 + 27 + function baperrverb () { 28 + if [ ! "$bap_verbosity" -ge 2 ]; then return 0; fi 29 + >&2 echo "bash-atproto: $*" 25 30 } 26 31 27 32 function bap_decodeJwt () { ··· 285 290 return 0 286 291 287 292 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 288 - bapecho "Looking up handle from $bap_handleResolveURL" 293 + baperrverb "Looking up handle from $bap_handleResolveURL" 289 294 bap_temp=$(curl -s -A "$bap_curlUserAgent" -G --data-urlencode "handle=$1" "$bap_handleResolveURL/xrpc/com.atproto.identity.resolveHandle" | jq -re .did) 290 295 if [ "$?" != "0" ]; then 291 296 baperr "Error obtaining DID from API" ··· 308 313 bapecho "Using DID: $savedDID" 309 314 return 0 310 315 } 316 + 317 + function bap_getRecord () { 318 + # get did of user 319 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "bash-atproto: fetching did..."; fi 320 + bap_temp[0]=$(bap_resolveDID $(echo $1 | cut -d '/' -f 3)) || { baperr "failed to fetch did of record creator"; return 1; } 321 + # get their pds 322 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "pds..."; fi 323 + bap_temp[1]=$(bap_resolvePDS ${bap_temp[0]}) || { baperr "failed to fetch pds of record creator"; return 1; } 324 + # get the post 325 + if [ "$bap_verbosity" -ge "2" ]; then >&2 echo -n "post..."; fi 326 + 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; } 327 + echo $bap_result 328 + baperrverb "ok" 329 + bap_result= 330 + return 0 331 + } 332 + 333 + function bap_getServiceAuth () { 334 + # Service, Lifetime, Lexicon 335 + if [ -z "$1" ]; then baperr "Required argument missing"; return 2; fi 336 + if ! bapInternal_validateDID $1 2> /dev/null; then baperr "input must be a DID"; return 1; fi 337 + bap_temp="aud=$1" 338 + if [ -n "$2" ]; then bap_temp="$bap_temp&exp=$(($2 + $(date +%s)))"; fi 339 + if [ -n "$3" ]; then bap_temp="$bap_temp&lxm=$3"; fi 340 + bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" -G -H "Authorization: Bearer $savedAccess" "$savedPDS/xrpc/com.atproto.server.getServiceAuth?$bap_temp") 341 + bapInternal_errorCheck $? bap_getServiceAuth "fatal: failed to mint service auth token" || return $? 342 + echo $bap_result | jq -r .token 343 + # PDS may change the expiry that bap requests 344 + if [ -n "$2" ]; then 345 + bap_decodeJwt "$(echo $bap_result | jq -r .token)" 346 + 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 347 + fi 348 + bap_result= 349 + return 0 350 + }
+15 -7
docs/HOWTO.md
··· 27 27 28 28 ## Create a Bluesky post 29 29 30 - This is the manual way to create a Bluesky post with bap-bsky. You can use `bapBsky_createPost` and `bapBsky_postImage` for basic posts, but manually writing the JSON with the CYOR commands allows for much more flexibility. 30 + This is the manual way to create a Bluesky post with bap-bsky. You can use `bapBsky_createPost`, `bapBsky_postImage` and `bapBsky_postVideo` for basic posts, but manually writing the JSON with the CYOR commands allows for much more flexibility. 31 31 32 32 1. `source bap-bsky.sh` 33 33 2. `bapBsky_cyorInit` ··· 47 47 48 48 ### Adding a video 49 49 50 - Right now, posting video is only possible with `bapBsky_postVideo`, which creates a post with fixed values that can't be customized with CYOR. 50 + You can embed a video in the post like this: 51 51 52 52 1. `bapBsky_prepareVideo <video file> <mime type>` 53 53 1. The mime type for MP4 video is `video/mp4`. Other video types should not be used as `bapBsky_postVideo` and the Bluesky embed lexicon hardcode `video/mp4`. 54 - 2. `bapBsky_postVideo $bap_postedBlob $bap_postedSize $bap_imageWidth $bap_imageHeight "<alt text, optional>" "<post text, optional>"` 54 + 2. `bapBsky_cyorAddVideo $bap_postedBlob $bap_postedSize $bap_imageWidth $bap_imageHeight "<alt text, optional>"` 55 55 56 56 ### Adding a self-label 57 57 ··· 65 65 * Adult - `porn` 66 66 * Graphic Media - `graphic-media` 67 67 68 + ### Replies 69 + 70 + You can set your post to be a reply to another with `bapBsky_cyorAddReply`. It takes a simple AT URI as input. 71 + 72 + * `bapBsky_cyorAddReply at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6ovsdood32z` 73 + 74 + This will also fetch the appropriate root post if it hasn't been added already. 75 + 68 76 ### Other things 69 77 70 - Because of how records work, you can add your own JSON keys, more formally called [unspecced fields](https://www.pfrazee.com/blog/lexicon-guidance#going-off-schema). You can use the `bapCYOR_str` and `bapCYOR_add` functions to do this. As the names imply, the former is for strings and the latter for integers. 78 + Because of how records work, it's possible to add your own JSON keys, more formally called [unspecced fields](https://www.pfrazee.com/blog/lexicon-guidance#going-off-schema). You can use the `bapCYOR_str` and `bapCYOR_add` functions to do this. As the names imply, the former is for strings and the latter for integers. 71 79 72 80 For example, if you wanted to secretly embed your pizza preferences in a post, you might run a command like `bapCYOR_str pizzaPreference "with-pineapple"`. This won't be visible in-app, but anyone that views that post's JSON (with [PDSls](https://pdsls.dev), for example) will be able to see your preference for pineapple on pizza. 73 81 ··· 90 98 91 99 ### Post a video 92 100 93 - Currently this is limited: No CYOR function for video (yet), and you can't add alt text. There's no Spark-dedicated prepare video command either, so try this: 101 + There's no Spark-dedicated prepare video command yet, but it's still pretty similar to Bluesky: 94 102 95 - 1. `bap_postBlobToPDS <video file> video/mp4.` 103 + 1. `bap_postBlobToPDS <video file> video/mp4` 96 104 1. Spark might support other video types, but it's better to just use mp4. You might want to scrub metadata before posting. 97 - 2. `bapSprk_postVideo $bap_postedBlob $bap_postedSize "<post text, optional>"` 105 + 2. `bapSprk_cyorAddVideo $bap_postedBlob $bap_postedSize "<alt text, optional>"`
+17
docs/MISSING.md
··· 1 + Current list of missing features 2 + 3 + ## bap-bsky 4 + 5 + * Communication with Lumi (Bluesky video service) 6 + 7 + * Tags (the element, not the facet) 8 + 9 + * External embeds 10 + 11 + * Facets 12 + 13 + * I'm not doing this lol 14 + 15 + ## bap-sprk 16 + 17 + To be determined...