+47
-25
README.md
+47
-25
README.md
···
1
1
# bash-atproto
2
2
3
-
bash-atproto is a bare-bones atproto client 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://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.
4
4
5
-
It supports the following operations (most API calls are done to the account's PDS):
6
-
7
-
* Resolving a handle to did:plc/did:web
8
-
9
-
* Resolving an account's PDS from the DID
10
-
11
-
* Creating and closing sessions on the PDS
5
+
## Supported functions
12
6
13
-
* Saving and loading a secrets file (contains your access and refresh tokens)
7
+
Functions built into `bash-atproto.sh`
14
8
15
-
* Extracting account and token information from the access token
9
+
* Resolving a handle to did:plc/did:web - `bap_didInit`
10
+
* Resolving an account's PDS from the DID - `bap_findPDS`
11
+
* Creating and closing sessions on the PDS - `bap_getKeys` and `bap_closeSession`
12
+
* Saving and loading a secrets file (contains your access and refresh tokens) - `bap_saveSecrets` and `bap_loadSecrets`
13
+
* Extracting account and token information from the access token - (automatic)
14
+
* Refreshing tokens - `bap_refreshKeys`
15
+
* Prepare an image, stripping EXIF data and (if needed) resizing it for a set file size and dimensions - `bap_prepareImage`
16
+
* Uploading blobs - `bap_uploadBlobToPDS`
17
+
* Working with records:
18
+
* Add string value - `bapCYOR_str`
19
+
* Add value - `bapCYOR_add`
20
+
* Add to array - `bapCYOR_arr`
21
+
* Remove key - `bapCYOR_rem`
22
+
* Post record - `bap_postRecord`
16
23
17
-
* Refreshing tokens
24
+
### Bluesky functions
18
25
19
-
* Creating basic Bluesky text post records
26
+
With `bap-bsky.sh`
20
27
21
-
* Creating Bluesky repost records
28
+
* Creating basic Bluesky text post records - `bapBsky_createPost`
29
+
* Creating Bluesky repost records - `bapBsky_createRepost`
30
+
* Preparing an image for Bluesky (including resizing and compressing) - `bapBsky_prepareImage`
31
+
* Creating a post with a single embedded image or video with alt text and embedded image dimensions - `bapBsky_postImage` and `bapBsky_postVideo`
32
+
* When working on a post:
33
+
* Start a post - `bapBsky_cyorInit`
34
+
* Prepare an image for upload - `bapBsky_prepareImage`
35
+
* Add an image - `bapBsky_cyorAddImage`
36
+
* Add a video - `bapBsky_cyorAddVideo`
37
+
* Set language - `bapBsky_cyorAddLangs`
38
+
* Add tags - `bapBsky_cyorAddTags`
39
+
* Add self-labels - `bapBsky_cyorAddSelfLabels`
40
+
* Make reply - `bapBsky_cyorAddReply`
41
+
* Make quote - `bapBsky_cyorAddQuote`
42
+
* Submit post - `bapBsky_submitPost`
22
43
23
-
* Preparing an image for Bluesky (including resizing and compressing)
44
+
### Spark functions
24
45
25
-
* Uploading blobs
46
+
With the **experimental** `bap-sprk.sh`
26
47
27
-
* Creating a post with a single embedded image with alt text
48
+
* Creating a post with a single embedded image with alt text or video - `bapSprk_postImage` and `bapSprk_postVideo`
49
+
* When working on a post:
50
+
* Start a post - `bapSprk_cyorInit`
51
+
* Prepare an image for upload - `bapSprk_prepareImage`
52
+
* Add an image - `bapSprk_cyorAddImage`
53
+
* Add a video - `bapSprk_cyorAddVideo`
54
+
* Submit post - `bapSprk_submitPost`
28
55
29
-
### Dependencies
56
+
## Dependencies
30
57
31
-
bash-atproto requires cURL 7.76 or later and jq. Posting images (not used by 765coverbot) additionally requires `imagemagick`, `exiftool` and `uuidgen`.
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`.
32
59
33
60
## Basic usage
34
61
35
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:
36
63
37
64
1. `bap_didInit <did or handle>` which will resolve your handle to a DID
38
-
39
65
2. `bap_findPDS $savedDID` which will retrieve the account's PDS for use
40
-
41
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.
67
+
68
+
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`.
42
69
43
70
If bash-atproto is being used with a bot or other service that runs periodically, the calling application should implement a timer to refresh tokens. bash-atproto provides a function to perform a refresh (`bap_refreshKeys`) and a timestamp to detect when a token refresh should be performed (`$bap_savedAccessExpiry`), but it does not do so itself.
44
71
···
47
74
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:
48
75
49
76
1. `source bash-atproto.sh`
50
-
51
77
2. `bap_didInit <your did or handle>`
52
-
53
78
3. `bap_findPDS $savedDID`
54
-
55
79
4. `bap_getKeys $savedDID <app password>`
56
-
57
80
5. `bap_postToBluesky "Hello, World!" en`
58
-
59
81
6. `bap_closeSession`
60
82
61
83
## License
+349
bap-bsky.sh
+349
bap-bsky.sh
···
1
+
#!/bin/bash
2
+
# SPDX-License-Identifier: MIT
3
+
# bap-bsky.sh: the bash-atproto functions pertaining to Bluesky Social.
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
6
+
7
+
bapBsky_internalVersion=1
8
+
bapBsky_internalMinorVer=4
9
+
bapBsky_allowLegacyPre2=1
10
+
11
+
function bapBskyErr () {
12
+
>&2 echo "bap-bsky: $*"
13
+
}
14
+
15
+
function bapBskyEcho () {
16
+
if [ ! "$bap_verbosity" -ge 1 ]; then return 0; fi
17
+
echo "bap-bsky: $*"
18
+
}
19
+
20
+
function bapBsky_cyorInit () {
21
+
bap_cyorRecord="{}"
22
+
bapCYOR_str \$type app.bsky.feed.post
23
+
bapCYOR_str text ""
24
+
}
25
+
26
+
function bapBskyInternal_convertToRecordWithMedia () {
27
+
cyorTemp=$(echo "$bap_cyorRecord $(echo $bap_cyorRecord | jq -r .embed | echo {\"embed\": {\"$1\": $(</dev/stdin)}})" | jq -s add) || return 1
28
+
bap_cyorRecord=$cyorTemp
29
+
bapCYOR_str \$type "app.bsky.embed.recordWithMedia" .embed
30
+
bapCYOR_str \$type "app.bsky.embed.record" .embed.record
31
+
}
32
+
33
+
function bapBsky_cyorAddImage () {
34
+
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
37
+
# 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
46
+
}
47
+
48
+
function bapBsky_cyorAddVideo () {
49
+
# param:
50
+
# 1 - blob
51
+
# 2 - size
52
+
# 3 - width
53
+
# 4 - height
54
+
# 5 - alt text
55
+
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
66
+
}
67
+
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 () {
79
+
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
82
+
# 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
85
+
else
86
+
# 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
89
+
fi
90
+
return 0
91
+
}
92
+
93
+
function bapBsky_cyorAddReply () {
94
+
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
99
+
return 0
100
+
}
101
+
102
+
function bapBsky_cyorAddLangs () {
103
+
if [ -z "$1" ]; then bapBskyErr "error: Required arugment missing"; return 1; fi
104
+
bapCYOR_rem langs
105
+
local iter=0
106
+
while [ -n "$1" ]; do
107
+
bapCYOR_arr "$1" .langs.["$iter"] || return $?
108
+
((iter++))
109
+
# lexicon limit 3
110
+
if [ "$iter" -gt "2" ]; then break; fi
111
+
shift
112
+
done
113
+
return 0
114
+
}
115
+
116
+
function bapBsky_cyorAddTags () {
117
+
if [ -z "$1" ]; then bapBskyErr "error: Required arugment missing"; return 1; fi
118
+
bapCYOR_rem tags
119
+
local iter=0
120
+
while [ -n "$1" ]; do
121
+
bapCYOR_arr "$1" .tags.["$iter"] || return $?
122
+
((iter++))
123
+
# lexicon limit 8
124
+
if [ "$iter" -gt "7" ]; then break; fi
125
+
shift
126
+
done
127
+
return 0
128
+
}
129
+
130
+
function bapBsky_cyorAddSelfLabels () {
131
+
if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi
132
+
bapCYOR_str \$type com.atproto.label.defs#selfLabels .labels
133
+
bapCYOR_rem labels.values
134
+
local iter=0
135
+
while [ -n "$1" ]; do
136
+
bapCYOR_str val "$1" .labels.values.["$iter"]
137
+
((iter++))
138
+
shift
139
+
done
140
+
return 0
141
+
}
142
+
143
+
function bapBsky_cyorAddQuote () {
144
+
if [ -z "$1" ]; then bapBskyErr "error: Required argument missing"; return 1; fi
145
+
local bapBsky_temp cyorTemp l
146
+
bapBsky_temp=$(bap_getRecord $1) || return $?
147
+
case "$(echo $bap_cyorRecord | jq -r .embed.[\"\$type\"])" in
148
+
"null")
149
+
bapCYOR_str \$type "app.bsky.embed.record" .embed;;
150
+
"app.bsky.embed.record");;
151
+
"app.bsky.embed.recordWithMedia")
152
+
l=.record;;
153
+
*)
154
+
# convert to record with media
155
+
bapBskyInternal_convertToRecordWithMedia media
156
+
l=.record;;
157
+
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
160
+
return 0
161
+
}
162
+
163
+
function bapBsky_cyorAddExternalEmbed () {
164
+
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
171
+
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
176
+
fi
177
+
}
178
+
179
+
function bapBsky_submitPost () {
180
+
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
181
+
bap_postRecord "$bap_cyorRecord" || return $?
182
+
bap_cyorRecord=
183
+
uri=$(echo $bap_result | jq -r .uri)
184
+
cid=$(echo $bap_result | jq -r .cid)
185
+
return 0
186
+
}
187
+
188
+
function bapBsky_createPost () { #1: exception 2: refresh required
189
+
if [ -z "$1" ]; then bapBskyErr "fatal: No argument given to post"; return 1; fi
190
+
bapBsky_cyorInit
191
+
bapCYOR_str text "$1"
192
+
if [ -n "$2" ]; then bapBsky_cyorAddLangs $2; fi
193
+
bapBsky_submitPost || return $?
194
+
bapBskyEcho "Posted record at $uri"
195
+
return 0
196
+
}
197
+
198
+
199
+
function bapBsky_createRepost () { # arguments 1 is uri, 2 is cid. error codes same as postToBluesky
200
+
if [ -z "$2" ]; then bapBskyErr "fatal: Required argument missing"; return 1; fi
201
+
bap_cyorRecord=
202
+
bapCYOR_str \$type app.bsky.feed.repost
203
+
bapCYOR_str cid $2 .subject
204
+
bapCYOR_str uri $1 .subject
205
+
bapBsky_submitPost || return $?
206
+
bapBskyEcho "Repost record at $uri"
207
+
return 0
208
+
}
209
+
210
+
function bapBsky_prepareImage () {
211
+
bap_prepareImage "$1" 1000000 2000 2000
212
+
return $?
213
+
}
214
+
215
+
function bapBsky_postImage () { #1: exception 2: refresh required
216
+
# param:
217
+
# 1 - blob
218
+
# 2 - mimetype
219
+
# 3 - size
220
+
# 4 - width
221
+
# 5 - height
222
+
# 6 - alt text
223
+
# 7 - text
224
+
if [ -z "$5" ]; then bapBskyErr "fatal: more arguments required"; return 1; fi
225
+
bapBsky_cyorInit
226
+
bapBsky_cyorAddImage 0 $1 $2 $3 $4 $5 "$6"
227
+
bapCYOR_str text "$7"
228
+
bapBsky_submitPost || return $?
229
+
bapBskyEcho "Posted record at $uri"
230
+
return 0
231
+
}
232
+
233
+
function bapBsky_checkVideo () {
234
+
if [ ! -f "$1" ]; then bapBskyErr "error: specify file to check"; return 1; fi
235
+
if [[ $(stat -c %s "$1") -gt 100000000 ]]; then bapBskyErr 'fatal: video may not exceed 100 mb'; return 1; fi
236
+
if [ "$(exiftool -duration# -s3 "$1" | awk '{print int($1+0.5)}')" -gt "180" ]; then bapBskyErr "error: video length must be 3 minutes or less"; return 1; fi
237
+
return 0
238
+
}
239
+
240
+
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
+
# $1 is file
248
+
# $2 is mime (like bap_postBlobToPDS)
249
+
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
251
+
bapBsky_checkVideo "$1" || return $?
252
+
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
258
+
bap_postBlobToPDS "/tmp/$uuid.mp4" "$2" || { bapBskyErr "warning: video upload failed"; rm "/tmp/$uuid.mp4"; return 1; }
259
+
rm "/tmp/$uuid.mp4"
260
+
bap_imageWidth=$(exiftool -ImageWidth -s3 $1)
261
+
bap_imageHeight=$(exiftool -ImageHeight -s3 $1)
262
+
bapBskyEcho 'video "posted"'
263
+
return 0
264
+
}
265
+
266
+
function bapBsky_getVideoLimits () {
267
+
# TODO: figure out atproto-proxy for Lumi
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."
276
+
return 0
277
+
fi
278
+
}
279
+
280
+
function bapBsky_postVideo () {
281
+
# param:
282
+
# 1-5 - see bapBsky_cyorAddVideo
283
+
# 6 - text
284
+
if [ -z "$4" ]; then bapBskyErr "fatal: more arguments required"; return 1; fi
285
+
bapBsky_cyorInit
286
+
bapCYOR_str text "$6"
287
+
bapBsky_cyorAddVideo $1 $2 $3 $4 "$5"
288
+
bapBsky_submitPost || return $?
289
+
bapBskyEcho "Posted record at $uri"
290
+
return 0
291
+
}
292
+
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 $?
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 $?
337
+
}
338
+
339
+
function bap_prepareVideoForBluesky () {
340
+
bapBsky_stubWarn bapBsky_prepareVideo || return $?
341
+
bapBsky_prepareVideo "$@"
342
+
return $?
343
+
}
344
+
345
+
function bap_postVideoToBluesky () {
346
+
bapBsky_stubWarn bapBsky_postVideo || return $?
347
+
bapBsky_postVideo "$@"
348
+
return $?
349
+
}
+60
bap-extra.sh
+60
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=1
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
+
}
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
37
+
return 0
38
+
}
39
+
40
+
function bapExt_checkDeps () {
41
+
local problem=0
42
+
if ! type file >/dev/null 2>&1; then problem=2; bapExtErr "missing file"; fi
43
+
if ! type convert >/dev/null 2>&1; then problem=2; bapExtErr "missing convert"; fi
44
+
if ! type exiftool >/dev/null 2>&1; then problem=2; bapExtErr "missing exiftool"; fi
45
+
if ! type uuidgen >/dev/null 2>&1; then problem=2; bapExtErr "missing uuidgen"; fi
46
+
if ! type jq >/dev/null 2>&1; then problem=1; bapExtErr "missing jq"; fi
47
+
if ! type curl >/dev/null 2>&1; then problem=1; bapExtErr "missing curl"; fi
48
+
# if you can load this function, you have bash
49
+
50
+
case "$problem" in
51
+
1)
52
+
bapExtEcho "A core dependency required by bash-atproto is missing."
53
+
bapExtEcho "bash-atproto will not work.";;
54
+
2)
55
+
bapExtEcho "A dependency used by the media subsystems of bash-atproto is missing."
56
+
bapExtEcho "Processing images and video will not function, but other functionality will work.";;
57
+
0)
58
+
bapExtEcho "There are no bash-atproto dependency issues on this system.";;
59
+
esac
60
+
}
+76
-47
bap-sprk.sh
+76
-47
bap-sprk.sh
···
1
1
#!/bin/bash
2
2
# SPDX-License-Identifier: MIT
3
-
# bap-sprk.sh: Provides helper functions to post images and video to Spark Social.
4
-
# Requires bash-atproto (https://github.com/Engielolz/imasimgbot/blob/master/bash-atproto.sh)
3
+
# bap-sprk.sh: Spark the revolution...from the command line!
5
4
# The PDS is not capable of verifying Spark lexicon at the moment, be careful!
5
+
# This code has not been thoroughly tested. Use at your own risk!
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
6
8
7
-
function bapSprk_err() {
8
-
>&2 echo "bash-atproto: spark: $*"
9
+
10
+
bapSprk_internalVersion=2
11
+
bapSprk_internalMinorVer=0
12
+
13
+
function bapSprk_err () {
14
+
>&2 echo "bap-sprk: $*"
9
15
}
10
16
11
-
function bapSprk_echo() {
17
+
function bapSprk_echo () {
12
18
if [ ! "$bap_verbosity" -ge 1 ]; then return 0; fi
13
-
echo "bash-atproto: spark: $*"
19
+
echo "bap-sprk: $*"
20
+
}
21
+
22
+
function bapSprk_cyorInit () {
23
+
bap_cyorRecord="{}"
24
+
bapCYOR_str \$type so.sprk.feed.post
25
+
bapCYOR_str text ""
26
+
}
27
+
28
+
function bapSprk_cyorAddImage () {
29
+
# Lexicon has alt text but no image dimensions
30
+
# 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
41
+
}
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
+
62
+
function bapSprk_prepareImage () {
63
+
# 5MB image limit: at://sprk.so/app.bsky.feed.post/3lipdqef2k22n
64
+
# Get rid of dimensions liimt with really big numbers
65
+
bap_prepareImage "$1" 5000000 2147483647 2147483647
66
+
return $?
14
67
}
15
68
16
-
function bapSprk_postVideo() {
69
+
function bapSprk_postVideo () {
17
70
# The parameters are different commpared to bap_postVideoToBluesky
18
71
# If you just ran bap_postBlobToPDS, you can use the variables on the right.
19
72
# 1 blob - $bap_postedImage
20
73
# 2 size - $bap_postedSize
21
74
# 3 mime - $bap_postedMime
22
-
# 4 text - excluding may or may not be bad in the lexicon
23
-
# The lexicon I've seen don't have alt text or video dimensions...
24
-
# Also no multiple video, that must be coming later
75
+
# 4 alt text
76
+
# 5 post text
25
77
if [ -z "$3" ]; then bapSprk_err "fatal: more arguments required"; return 1; fi
26
-
bap_cyorRecord="{}"
27
-
bapCYOR_str text "$4"
28
-
bapCYOR_str \$type so.sprk.feed.post
29
-
bapCYOR_str \$type so.sprk.embed.video .embed
30
-
#bapCYOR_str alt "" .embed.video
31
-
bapCYOR_str \$type blob .embed.video
32
-
bapCYOR_str \$link $1 .embed.video.ref
33
-
bapCYOR_str mimeType "$bap_postedMime" .embed.video
34
-
bapCYOR_add size $bap_postedSize .embed.video
35
-
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
36
-
bap_postRecord "$bap_cyorRecord" || return $?
37
-
uri=$(echo $bap_result | jq -r .uri)
38
-
cid=$(echo $bap_result | jq -r .cid)
78
+
bapSprk_cyorInit
79
+
bapCYOR_str text "$5"
80
+
bapSprk_cyorAddVideo $1 $2 $3 "$4"
81
+
bapSprk_submitPost "$bap_cyorRecord" || return $?
39
82
bapSprk_echo "Posted record at $uri"
83
+
return 0
40
84
}
41
85
42
86
43
-
function bapSprk_postImage() {
44
-
# Trying to mirror bap_postImageToBluesky
45
-
# Lexicon has alt text but not image dimensions?
87
+
function bapSprk_postImage () {
88
+
# Trying to mirror bapBsky_postImage
46
89
# param:
47
90
# 1 - blob
48
91
# 2 - mimetype
49
92
# 3 - size
50
-
# 4 - width
51
-
# 5 - height
52
-
# 6 - alt text
53
-
# 7 - text
54
-
# image dimensions will be ignored, or specify ""
93
+
# 4 - alt text
94
+
# 5 - text
55
95
if [ -z "$3" ]; then bapSprk_err "fatal: more arguments required"; return 1; fi
56
-
bap_cyorRecord="{}"
57
-
bapCYOR_str text "$7"
58
-
bapCYOR_str \$type so.sprk.feed.post
59
-
bapCYOR_str \$type so.sprk.embed.images .embed
60
-
bapCYOR_str alt "$6" .embed.images.[0]
61
-
bapCYOR_str \$type so.sprk.embed.images#image .embed.images.[0]
62
-
bapCYOR_str \$type blob .embed.images.[0].image
63
-
bapCYOR_str \$link $1 .embed.images.[0].image.ref
64
-
bapCYOR_str mimeType "$2" .embed.images.[0].image
65
-
bapCYOR_add size $3 .embed.images.[0].image
66
-
#bapCYOR_add width $4 .embed.images.[0].aspectRatio
67
-
#bapCYOR_add height $5 .embed.images.[0].aspectRatio
68
-
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
69
-
bap_postRecord "$bap_cyorRecord" || return $?
70
-
uri=$(echo $bap_result | jq -r .uri)
71
-
cid=$(echo $bap_result | jq -r .cid)
96
+
bapSprk_cyorInit
97
+
bapCYOR_str text "$5"
98
+
bapSprk_cyorAddImage 0 $1 $2 $3 "$4" "$5"
99
+
bapSprk_submitPost "$bap_cyorRecord" || return $?
72
100
bapSprk_echo "Posted record at $uri"
101
+
return 0
73
102
}
+116
-145
bash-atproto.sh
+116
-145
bash-atproto.sh
···
1
1
#!/bin/bash
2
2
# SPDX-License-Identifier: MIT
3
+
bap_internalVersion=3
4
+
bap_internalMinorVer=3
3
5
4
6
# you can change these
5
7
bap_plcDirectory=https://plc.directory
6
8
bap_handleResolveURL=https://public.api.bsky.app
7
-
bap_curlUserAgent="curl/$(curl -V | awk 'NR==1{print $2}') bash-atproto/2-$(git -C $(dirname $BASH_SOURCE) -c safe.directory=$(dirname $BASH_SOURCE) 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) -c safe.directory=$(cd $(dirname $BASH_SOURCE); pwd) describe --always --dirty)"
8
10
bap_chmodSecrets=1
9
11
bap_verbosity=1
10
12
···
20
22
function bapverbose () {
21
23
if [ ! "$bap_verbosity" -ge 2 ]; then return 0; fi
22
24
echo "bash-atproto: $*"
25
+
}
26
+
27
+
function baperrverb () {
28
+
if [ ! "$bap_verbosity" -ge 2 ]; then return 0; fi
29
+
>&2 echo "bash-atproto: $*"
23
30
}
24
31
25
32
function bap_decodeJwt () {
···
66
73
case $1 in
67
74
0);;
68
75
22)
69
-
if [ ! -z "$3" ]; then baperr "$3"; fi
76
+
if [ -n "$3" ]; then baperr "$3"; fi
77
+
if ! jq -e . >/dev/null 2>&1 <<<"$bap_result"; then baperr "the server did not respond with valid JSON"; return 1; fi
70
78
APIErrorCode=$(echo $bap_result | jq -r .error)
71
79
if ! [ "$APIErrorCode" = "ExpiredToken" ]; then bapInternal_processAPIError $2 bap_result; return 1; fi
72
80
baperr 'the token needs to be refreshed'
73
81
return 2;;
74
82
*)
75
-
if [ ! -z "$3" ]; then baperr "$3"; fi
83
+
if [ -n "$3" ]; then baperr "$3"; fi
76
84
baperr "cURL threw exception $1 in function $2"
77
85
return 1;;
78
86
esac
···
81
89
function bapInternal_verifyStatus () {
82
90
if [ "$(echo $bap_result | jq -r .active)" = "false" ]; then
83
91
baperr "warning: account is inactive"
84
-
if [ ! -z "$(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
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
85
93
return 115
86
94
fi
87
95
}
···
101
109
savedRefresh=$(echo $bap_result | jq -r .refreshJwt)
102
110
# we don't care about the handle
103
111
bap_decodeJwt $savedAccess
104
-
if [ "$(echo $bap_jwt | jq -r .scope)" != "com.atproto.appPass" ]; then baperr "warning: this is not an app password"; fi
112
+
if [ "$(echo $bap_jwt | jq -r .scope)" = "com.atproto.access" ]; then baperr "warning: this is not an app password"; fi
105
113
bapInternal_verifyStatus || return $?
106
114
return 0
107
115
}
···
131
139
# for quotes
132
140
if [ -z "$1" ]; then baperr "nothing to add"; return 1; fi
133
141
if [ -z "$bap_cyorRecord" ]; then bap_cyorRecord="{}"; fi
134
-
bap_temp=$2
135
-
bap_cyorRecord=$(echo "$bap_cyorRecord" | jq -c "$3.[\"$1\"]=\"$bap_temp\"")
142
+
local bap_temp
143
+
bap_temp=$(echo "$bap_cyorRecord" | jq -c "$3.[\"$1\"]=\"$2\"") || return $?
144
+
bap_cyorRecord=$bap_temp
136
145
return $?
137
146
}
138
147
···
140
149
# for things that shouldn't be in quotes
141
150
if [ -z "$1" ]; then baperr "nothing to add"; return 1; fi
142
151
if [ -z "$bap_cyorRecord" ]; then bap_cyorRecord="{}"; fi
143
-
bap_temp=$2
144
-
bap_cyorRecord=$(echo "$bap_cyorRecord" | jq -c "$3.[\"$1\"]=$bap_temp")
152
+
local bap_temp
153
+
bap_temp=$(echo "$bap_cyorRecord" | jq -c "$3.[\"$1\"]=$2") || return $?
154
+
bap_cyorRecord=$bap_temp
145
155
return $?
146
156
}
147
157
148
-
function bapCYOR_rem () {
149
-
# doesn't handle special names atm
150
-
if [ -z "$1" ] || [ -z "bap_cyorRecord" ]; then baperr "nothing to remove"; return 1; fi
151
-
bap_cyorRecord=$(echo $bap_cyorRecord | jq -c "del(.$1)")
158
+
function bapCYOR_arr () {
159
+
# arrays
160
+
if [ -z "$1" ]; then baperr "nothing to add"; return 1; fi
161
+
if [ -z "$bap_cyorRecord" ]; then bap_cyorRecord="{}"; fi
162
+
local bap_temp
163
+
bap_temp=$(echo "$bap_cyorRecord" | jq -c "$2=\"$1\"") || return $?
164
+
bap_cyorRecord=$bap_temp
152
165
return $?
153
166
}
154
167
155
-
function bapCYOR_bskypost () {
156
-
bap_cyorRecord="{}"
157
-
bapCYOR_str \$type app.bsky.feed.post
158
-
bapCYOR_str text ""
168
+
function bapCYOR_rem () {
169
+
# doesn't handle special names atm
170
+
if [ -z "$1" ] || [ -z "$bap_cyorRecord" ]; then baperr "nothing to remove"; return 1; fi
171
+
local bap_temp
172
+
bap_temp=$(echo $bap_cyorRecord | jq -c "del(.$1)") || return $?
173
+
bap_cyorRecord=$bap_temp
174
+
return $?
159
175
}
160
176
161
177
function bapInternal_finalizeRecord () {
···
174
190
175
191
function bap_postToBluesky () { #1: exception 2: refresh required
176
192
if [ -z "$1" ]; then baperr "fatal: No argument given to post"; return 1; fi
177
-
bapCYOR_bskypost
193
+
bap_cyorRecord='{"$type":"app.bsky.feed.post"}'
178
194
bapCYOR_str text "$1"
179
-
if ! [ -z "2" ]; then bapCYOR_add langs "[\"$2\"]"; fi
195
+
if [ -n "$2" ]; then bapCYOR_add langs "[\"$2\"]"; fi
180
196
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
181
197
bap_postRecord "$bap_cyorRecord" || return $?
182
198
uri=$(echo $bap_result | jq -r .uri)
···
185
201
return 0
186
202
}
187
203
188
-
function bap_repostToBluesky () { # arguments 1 is uri, 2 is cid. error codes same as postToBluesky
189
-
if [ -z "$2" ]; then baperr "fatal: Required argument missing"; return 1; fi
190
-
bap_cyorRecord=
191
-
bapCYOR_str \$type app.bsky.feed.repost
192
-
bapCYOR_str cid $2 .subject
193
-
bapCYOR_str uri $1 .subject
194
-
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
195
-
bap_postRecord "$bap_cyorRecord" || return $?
196
-
uri=$(echo $bap_result | jq -r .uri)
197
-
cid=$(echo $bap_result | jq -r .cid)
198
-
bapecho "Repost record at $uri"
199
-
return 0
200
-
}
201
-
202
-
function bapHelper_resizeImageForBluesky () {
204
+
function bapInternal_resizeImage () {
203
205
bapecho "need to resize image"
204
-
convert /tmp/bash-atproto/$workfile -resize 2000x2000 /tmp/bash-atproto/new-$workfile
206
+
convert /tmp/bash-atproto/$workfile -resize $1x$2 /tmp/bash-atproto/new-$workfile
205
207
if ! [ "$?" = "0" ]; then baperr "fatal: convert failed!"; rm /tmp/bash-atproto/$workfile 2>/dev/null; return 1; fi
206
208
mv -f /tmp/bash-atproto/new-$workfile /tmp/bash-atproto/$workfile
207
209
}
208
210
209
-
function bapHelper_compressImageForBluesky () {
211
+
function bapInternal_compressImage () {
210
212
bapecho "image is too big, trying to compress"
211
-
convert /tmp/bash-atproto/$workfile -define jpeg:extent=1000kb /tmp/bash-atproto/new-${workfile%.*}.jpg
212
-
if [[ ! "$?" = "0" ]] || [[ $(stat -c %s /tmp/bash-atproto/new-${workfile%.*}.jpg) -gt 1000000 ]]; then baperr "fatal: error compressing image or image too big to fit in skeet"; rm /tmp/bash-atproto/$workfile /tmp/bash-atproto/new-${workfile%.*}.jpg; return 1; fi
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
213
215
rm /tmp/bash-atproto/$workfile
214
216
mv -f /tmp/bash-atproto/new-${workfile%.*}.jpg /tmp/bash-atproto/${workfile%.*}.jpg
215
217
workfile=${workfile%.*}.jpg
216
218
}
217
219
218
-
function bap_prepareImageForBluesky () { # 1: error 2 missing dep
219
-
if [ -z "$1" ]; then baperr "fatal: no image specified to prepare"; return 1; fi
220
+
function bap_prepareImage () { # 1: error 2 missing dep
221
+
# args: 1 - image, 2 - max size, 3 - max width, 4 - max height
222
+
if [ -z "$4" ]; then baperr "fatal: not enough parameters"; return 1; fi
223
+
if [ ! -f "$1" ]; then baperr "fatal: image not found"; return 1; fi
220
224
mkdir /tmp/bash-atproto 2>/dev/null
221
225
workfile=$(uuidgen)."${1##*.}"
222
-
cp $1 /tmp/bash-atproto/$workfile
226
+
cp "$1" /tmp/bash-atproto/$workfile
223
227
exiftool -all= /tmp/bash-atproto/$workfile -overwrite_original
224
228
if ! [ "$?" = "0" ]; then baperr "fatal: exiftool failed!"; rm /tmp/bash-atproto/$workfile 2>/dev/null; return 1; fi
225
-
if [[ $(identify -format '%w' /tmp/bash-atproto/$workfile) -gt 2000 ]] || [[ $(identify -format '%h' /tmp/bash-atproto/$workfile) -gt 2000 ]]; then
226
-
bapHelper_resizeImageForBluesky
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
227
231
if ! [ "$?" = "0" ]; then return 1; fi
228
232
fi
229
-
if [[ $(stat -c %s /tmp/bash-atproto/$workfile) -gt 1000000 ]]; then
230
-
bapHelper_compressImageForBluesky
233
+
if [[ $(stat -c %s /tmp/bash-atproto/$workfile) -gt $2 ]]; then
234
+
bapInternal_compressImage $2
231
235
if ! [ "$?" = "0" ]; then return 1; fi
232
236
fi
233
237
bapecho "image preparation successful"
···
253
257
return 0
254
258
}
255
259
256
-
function bap_postImageToBluesky () { #1: exception 2: refresh required
257
-
# param:
258
-
# 1 - blob
259
-
# 2 - mimetype
260
-
# 3 - size
261
-
# 4 - width
262
-
# 5 - height
263
-
# 6 - alt text
264
-
# 7 - text
265
-
if [ -z "$5" ]; then baperr "fatal: more arguments required"; return 1; fi
266
-
# it's easy but just LOOK at all those commands
267
-
bapCYOR_bskypost
268
-
bapCYOR_str text "$7"
269
-
bapCYOR_str \$type app.bsky.embed.images .embed
270
-
bapCYOR_str alt "$6" .embed.images.[0]
271
-
bapCYOR_str \$type blob .embed.images.[0].image
272
-
bapCYOR_str \$link $1 .embed.images.[0].image.ref
273
-
bapCYOR_str mimeType "$2" .embed.images.[0].image
274
-
bapCYOR_add size $3 .embed.images.[0].image
275
-
bapCYOR_add width $4 .embed.images.[0].aspectRatio
276
-
bapCYOR_add height $5 .embed.images.[0].aspectRatio
277
-
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
278
-
bap_postRecord "$bap_cyorRecord" || return $?
279
-
uri=$(echo $bap_result | jq -r .uri)
280
-
cid=$(echo $bap_result | jq -r .cid)
281
-
bapecho "Posted record at $uri"
282
-
return 0
283
-
}
284
-
285
-
function bap_checkVideoForBluesky () {
286
-
if [ ! -f $1 ]; then baperr "error: specify file to check"; return 1; fi
287
-
if [[ $(stat -c %s $1) -gt 100000000 ]]; then baperr 'fatal: video may not exceed 100 mb'; return 1; fi
288
-
if [ "$(exiftool -duration# -s3 $1 | awk '{print int($1+0.5)}')" -gt "180" ]; then baperr "error: video length must be 3 minutes or less"; return 1; fi
289
-
return 0
290
-
}
291
-
292
-
function bap_prepareVideoForBluesky () {
293
-
# stub, will actually talk to bluesky video service in the future
294
-
# $1 is file
295
-
# $2 is mime (like bap_postBlobToPDS)
296
-
if [ -z "$2" ]; then baperr "fatal: Required argument missing"; return 1; fi
297
-
bap_checkVideoForBluesky "$1" || return $?
298
-
bap_postBlobToPDS $1 $2
299
-
if [ "$?" != "0" ]; then baperr "warning: video upload failed"; return 1; fi
300
-
bap_imageWidth=$(exiftool -ImageWidth -s3 $1)
301
-
bap_imageHeight=$(exiftool -ImageHeight -s3 $1)
302
-
bapecho 'video "posted"'
303
-
return 0
304
-
}
305
-
306
-
function bap_postVideoToBluesky () {
307
-
# param:
308
-
# 1 - blob
309
-
# 2 - size
310
-
# 3 - width
311
-
# 4 - height
312
-
# 5 - alt text
313
-
# 6 - text
314
-
# assuming video/mp4 is always the mimetype might be a bad assumption
315
-
if [ -z "$4" ]; then baperr "fatal: more arguments required"; return 1; fi
316
-
bapCYOR_bskypost
317
-
bapCYOR_str text "$6"
318
-
bapCYOR_str alt "$5" .embed
319
-
bapCYOR_str \$type app.bsky.embed.video .embed
320
-
bapCYOR_str \$type blob .embed.video
321
-
bapCYOR_str \$link "$1" .embed.video.ref
322
-
bapCYOR_str mimeType "video/mp4" .embed.video
323
-
bapCYOR_add size $2 .embed.video
324
-
bapCYOR_add width $3 .embed.aspectRatio
325
-
bapCYOR_add height $4 .embed.aspectRatio
326
-
bapCYOR_str createdAt $(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
327
-
bap_postRecord "$bap_cyorRecord"
328
-
bapInternal_errorCheck $? bap_postVideoToBluesky "error: post failed" || return $?
329
-
uri=$(echo $bap_result | jq -r .uri)
330
-
cid=$(echo $bap_result | jq -r .cid)
331
-
bapecho "Posted record at $uri"
332
-
return 0
333
-
}
334
-
335
-
function bap_findPDS () {
260
+
function bap_resolvePDS () {
336
261
if [ -z "$1" ]; then baperr "fatal: no did specified"; return 1; fi
337
262
bapInternal_validateDID "$1" || return 1
338
263
case "$(echo $1 | cut -d ':' -f 2)" in
339
264
340
265
"plc")
341
266
bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" "$bap_plcDirectory/$1")
342
-
bapInternal_errorCheck $? bap_findPDS "fatal: did:plc lookup failed" || return $?
267
+
bapInternal_errorCheck $? bap_resolvePDS "fatal: did:plc lookup failed" || return $?
343
268
;;
344
269
345
270
"web")
346
271
bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" "$(echo https://$1 | sed 's/did:web://g')/.well-known/did.json")
347
-
bapInternal_errorCheck $? bap_findPDS "fatal: did:web lookup failed" || return $?
272
+
bapInternal_errorCheck $? bap_resolvePDS "fatal: did:web lookup failed" || return $?
348
273
;;
349
274
350
275
*)
···
360
285
((iter+=1))
361
286
continue
362
287
fi
363
-
savedPDS=$(echo "$bap_resolve" | jq -r ".[$iter].serviceEndpoint")
288
+
echo "$bap_resolve" | jq -r ".[$iter].serviceEndpoint"
364
289
break
365
290
done <<< "$(echo "$bap_resolve" | jq -r .[].id)"
366
-
if [ -z "$savedPDS" ]; then baperr "fatal: PDS not found in DID document"; return 1; fi
367
291
return 0
368
292
}
369
293
370
-
function bap_didInit () {
371
-
if [ -z "$1" ]; then baperr "specify identifier as first parameter"; return 1; fi
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
297
+
return 0
298
+
}
372
299
373
-
if bapInternal_validateDID $1 2> /dev/null; then
374
-
savedDID=$1
375
-
bapecho "Using user-specified DID: $savedDID"
300
+
function bap_resolveDID () {
301
+
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
376
304
return 0
377
305
378
-
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
379
-
bapecho "Looking up handle from $bap_handleResolveURL"
380
-
savedDID=$(curl -s -A "$bap_curlUserAgent" -G --data-urlencode "handle=$1" "$bap_handleResolveURL/xrpc/com.atproto.identity.resolveHandle" | jq -re .did)
381
-
if [ "$?" != "0" ]; then
382
-
baperr "Error obtaining DID from API"
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
312
+
fi
313
+
echo $bap_temp
314
+
return 0
315
+
316
+
else
317
+
baperr "fatal: input not a handle or did"
383
318
return 1
319
+
384
320
fi
385
-
bapecho "Using DID from API: $savedDID"
321
+
# should not get here
322
+
return 1
323
+
}
386
324
387
-
else
388
-
baperr "fatal: input not a handle or did"
389
-
return 1
325
+
function bap_didInit () {
326
+
savedDID=$(bap_resolveDID "$1") || return $?
327
+
bapecho "Using DID: $savedDID"
328
+
return 0
329
+
}
330
+
331
+
function bap_getRecord () {
332
+
# get did of user
333
+
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; }
335
+
# get their pds
336
+
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; }
338
+
# 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=
344
+
return 0
345
+
}
390
346
391
-
fi
392
-
return 0
347
+
function bap_getServiceAuth () {
348
+
# Service, Lifetime, Lexicon
349
+
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
351
+
bap_temp="aud=$1"
352
+
if [ -n "$2" ]; then bap_temp="$bap_temp&exp=$(($2 + $(date +%s)))"; fi
353
+
if [ -n "$3" ]; then bap_temp="$bap_temp&lxm=$3"; fi
354
+
bap_result=$(curl -s --fail-with-body -A "$bap_curlUserAgent" -G -H "Authorization: Bearer $savedAccess" "$savedPDS/xrpc/com.atproto.server.getServiceAuth?$bap_temp")
355
+
bapInternal_errorCheck $? bap_getServiceAuth "fatal: failed to mint service auth token" || return $?
356
+
echo $bap_result | jq -r .token
357
+
# PDS may change the expiry that bap requests
358
+
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
361
+
fi
362
+
bap_result=
363
+
return 0
393
364
}
+126
docs/HOWTO.md
+126
docs/HOWTO.md
···
1
+
# bash-atproto How To
2
+
3
+
This is a guide on using bash-atproto and its core addons bap-bsky and bap-sprk.
4
+
5
+
As a reminder, bash-atproto is meant for use as a library to be used by other scripts rather than a standalone client for interactive use. You can still use it like that, but writing another script to do the work for you is the better idea long term.
6
+
7
+
## Log in
8
+
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>`
13
+
14
+
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
+
16
+
The rest of these commands assume you have already sourced bash-atproto and logged in.
17
+
18
+
### Refreshing
19
+
20
+
The access token you get after logging in will expire shortly after being minted. The duration is PDS specific, but the default for the Bluesky PDS is 2 hours. Fortunately, the refresh token (which takes much longer to expire) can be used to mint another.
21
+
22
+
You can refresh the tokens at any time with `bap_refreshKeys`. If you previously wrote the tokens to disk, be sure to do so again, as the old refresh token it contains will no longer work!
23
+
24
+
### Ending a session
25
+
26
+
You can close the session with `bap_closeSession`. After this, the tokens are cleared from memory and the refresh token will no longer work. Tokens saved to disk will remain; it's good practice to clean them up.
27
+
28
+
## Create a Bluesky post
29
+
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
+
32
+
To create a simple post in English:
33
+
34
+
1. `source bap-bsky.sh`
35
+
2. `bapBsky_cyorInit`
36
+
3. `bapCYOR_str text "It's my first post!"`
37
+
4. `bapBsky_cyorAddLangs en`
38
+
5. `bapBsky_submitPost`
39
+
40
+
### Adding languages
41
+
42
+
`bapBsky_cyorAddLangs` can add language tags to your post. You can specify up to 3 (per the lexicon rules), separating additional tags as separate parameters:
43
+
44
+
* `bapBsky_cyorAddLangs en ja pt` to mark a post as having English, Japanese and Portuguese.
45
+
46
+
### Adding an image
47
+
48
+
Before submitting the post, run these commands:
49
+
50
+
1. `bapBsky_prepareImage <image file>`
51
+
2. `bap_postBlobToPDS $bap_preparedImage $bap_preparedMime`
52
+
1. These variables are prepared by the previous command.
53
+
3. `bapBsky_cyorAddImage # $bap_postedBlob $bap_postedMime $bap_postedSize $bap_imageWidth $bap_imageHeight "<alt text, optional>"`
54
+
1. `#` is the index number of the image. Counting starts at 0 and you can add up to 4, so 0-3 are acceptable values.
55
+
2. Like before, all the `$bap_*` variables are prepared for you by the previous commands. You may optionally specify alt text to go with the image.
56
+
57
+
### Adding a video
58
+
59
+
You can embed a video in the post like this:
60
+
61
+
1. `bapBsky_prepareVideo <video file> <mime type>`
62
+
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`.
63
+
2. `bapBsky_cyorAddVideo $bap_postedBlob $bap_postedSize $bap_imageWidth $bap_imageHeight "<alt text, optional>"`
64
+
65
+
### Adding a self-label
66
+
67
+
This can be done any time before submission, but ideally you'd do it after adding the embed.
68
+
69
+
1. `bapBsky_cyorAddSelfLabels <labels>`
70
+
1. You can specify multiple labels as separate parameters.
71
+
2. The official Bluesky app maps the self-labels with these names:
72
+
* Suggestive - `sexual`
73
+
* Nudity - `nudity`
74
+
* Adult - `porn`
75
+
* Graphic Media - `graphic-media`
76
+
77
+
### Replies
78
+
79
+
You can set your post to be a reply to another with `bapBsky_cyorAddReply`. It takes a simple AT URI as input.
80
+
81
+
* `bapBsky_cyorAddReply at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6ovsdood32z`
82
+
83
+
This will also fetch the appropriate root post if it hasn't been added already.
84
+
85
+
### Quote posts
86
+
87
+
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
+
89
+
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.
90
+
91
+
### Adding (hidden hash)tags
92
+
93
+
A little known feature of the Bluesky post lexicon is the ability to add hidden hashtags. These don't appear in the post body, but they still work for discovery and search just like normal hashtags that are added with a facet. You can add up to 8 (again, lexicon limit) tags to your post with `bapBsky_cyorAddTags`:
94
+
95
+
* `bapBsky_cyorAddTags Bluesky ATProto`
96
+
97
+
### Other things
98
+
99
+
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.
100
+
101
+
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.
102
+
103
+
## Spark
104
+
105
+
Creating Spark posts is a lot like creating Bluesky posts, even the lexicon is similar! Though as you might expect from an image and video platform, you'll need to add an appropriate embed if you want your post to work.
106
+
107
+
1. `source bap-sprk.sh`
108
+
2. `bapSprk_cyorInit`
109
+
3. `bapCYOR_str text "<post text, optional>"`
110
+
4. Add an embed (pick below)
111
+
5. `bapSprk_submitPost`
112
+
113
+
### Post an image
114
+
115
+
1. `bapSprk_prepareImage <image file>`
116
+
2. `bap_postBlobToPDS $bap_preparedImage $bap_preparedMime`
117
+
3. `bapSprk_cyorAddImage # $bap_postedBlob $bap_postedMime $bap_postedSize "<alt text, optional>"`
118
+
1. `#` is the index number of the image. Unlike Bluesky, you can add up to 12! (0-11)
119
+
120
+
### Post a video
121
+
122
+
There's no Spark-dedicated prepare video command yet, but it's still pretty similar to Bluesky:
123
+
124
+
1. `bap_postBlobToPDS <video file> video/mp4`
125
+
1. Spark might support other video types, but it's better to just use mp4. You might want to scrub metadata before posting.
126
+
2. `bapSprk_cyorAddVideo $bap_postedBlob $bap_postedSize "<alt text, optional>"`
+14
docs/MISSING.md
+14
docs/MISSING.md