+17
-13
README.md
+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
+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
+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
+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
+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
+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