···55dependencies:
66 nixpkgs:
77 - atproto-goat
88+ - babashka
89910steps:
1011 - name: Sync posts
1112 command: |
1212- goat account login --username did:plc:4ijrxutxndrcbmwd2bzchsum
1313- goat bsky post "me when test CI and secrets by setting up a pipeline to make this post when i push this commit to main"
1313+ bb publish.bb
+272
publish.bb
···11+#!/usr/bin/env bb
22+(require '[babashka.curl :as curl]
33+ '[babashka.fs :as fs]
44+ '[babashka.process :refer [shell]]
55+ '[cheshire.core :as json]
66+ '[clojure.string :as str])
77+88+;; (def repo-details {:collection "com.whtwnd.blog.entry"})
99+(def collection-nsids {:ww "com.whtwnd.blog.entry"
1010+ :m1 "dev.m1emi1em.blog.postRef"})
1111+1212+(def default-shell!!-args {:out :string})
1313+1414+(defn shell!
1515+ "My guts tells me there's an easier way to wrap this involving :arglists metadata but I'm not entirely sure how that works so yolo"
1616+ [cmd-or-opts & args]
1717+ (let [defaults? (not (instance? clojure.lang.PersistentArrayMap cmd-or-opts))
1818+ opts (cond-> default-shell!!-args
1919+ (not defaults?) (merge cmd-or-opts))
2020+ args (cond->> args
2121+ defaults? (cons cmd-or-opts))]
2222+ (:out (apply shell opts args))))
2323+;; goat helpers
2424+2525+(defn goat-login
2626+ "Username and password are picked up from the ATP_AUTH_USERNAME and ATP_AUTH_PASSWORD env vars"
2727+ [] (shell! "goat account login"))
2828+2929+(defn goat-ls-coll [repo collection]
3030+ (->> (shell! "goat ls --collection" collection repo)
3131+ str/split-lines
3232+ (map #(->> (str/split % #"\t")
3333+ rest
3434+ (zipmap [:rkey :cid])))))
3535+3636+(defn goat-get-record [repo collection rkey]
3737+ (do (println "Getting record " rkey " from collection " collection " in " repo)
3838+ (if (nil? rkey)
3939+ {}
4040+ (-> (shell! "goat record get"
4141+ (str "at://" repo "/" collection "/" rkey))
4242+ json/parse-string
4343+ (update-keys keyword))
4444+ )))
4545+4646+(defn goat-create-record [rkey fname]
4747+ (do (println "Creating record " rkey " from " fname)
4848+ (shell! "goat record create -r" rkey "-n" fname)))
4949+5050+(defn goat-update-record [rkey fname]
5151+ (do (println "Updating record " rkey " from " fname)
5252+ (shell! "goat record update -r" rkey "-n" fname)))
5353+5454+(defn goat-delete-record [collection rkey]
5555+ (do (println "Deleting record " rkey " from " collection))
5656+ (if (nil? rkey)
5757+ nil
5858+ (shell! (str "goat record delete -c " collection " -r " rkey))))
5959+6060+;; TID Stuff
6161+;; Loosely adapted from https://github.com/BlushSocial/atproto-tid
6262+;; Not written with concurrency in mind
6363+;; Or error handling (what can possibly go wrong?)
6464+6565+(defn gen-timestamp []
6666+ ;; Not actually sure why we multiply this here but rolling with it
6767+ (->> (java.util.Date.) .getTime (* 1000)))
6868+6969+(def s32-char (vec "234567abcdefghijklmnopqrstuvwxyz"))
7070+7171+(defn base32 [n padding]
7272+ (let [pieces (->> n
7373+ (iterate #(quot % 32))
7474+ (take-while #(> % 0))
7575+ (map #(mod % 32)))]
7676+ (as-> pieces $
7777+ (map (partial nth s32-char) $)
7878+ (reverse $)
7979+ (concat $ (repeat \2))
8080+ (take padding $)
8181+ (apply str $))))
8282+8383+;; I think this would be more readable but I'm too lazy to make sure it works rn
8484+;; (-> pieces
8585+;; (->> (map #(nth s32-char %)) reverse)
8686+;; (concat (repeat \2))
8787+;; (->> (take padding) (apply str))
8888+8989+(defn unbase32 [tid]
9090+ (let [table (->> s32-char (map-indexed #(vector %2 %1)) (into {}))
9191+ lookup #(get table %)]
9292+ (->> tid
9393+ (map lookup)
9494+ (reduce #(+ (* %1 32) %2) 0))))
9595+9696+(defn gen-tid
9797+ ([] (gen-tid (gen-timestamp) (rand-int 1023)))
9898+ ([timestamp clockid]
9999+ (str (base32 timestamp 11) (base32 clockid 2))))
100100+101101+(defn parse-tid [tid]
102102+ {:timestamp (unbase32 (subs tid 0 11))
103103+ :clockid (unbase32 (subs tid 11 13))})
104104+105105+(defn stringify-tid
106106+ "I'm just gonna use date for this lol"
107107+ [tid]
108108+ (let [{:keys [timestamp clockid]} (parse-tid tid)
109109+ unix-timestamp (-> timestamp (quot 1000) (quot 1000)) ;; Dividing by 1000 twice since we multiplied by it once when making the TID, and also because java.util.Date.getTime() returns milliseconds but date uses seconds
110110+ date-format-str "+%Y-%m-%dT%H:%M:%S"
111111+112112+ time-string
113113+ (-> (shell {:out :string}
114114+ "date -d"
115115+ (str "@" unix-timestamp)
116116+ date-format-str)
117117+ :out str/trim)]
118118+ (format "%s.%03dZ" time-string clockid)))
119119+120120+;;;
121121+122122+(defn blog-record [title content visibility tid]
123123+ {:$type (:ww collection-nsids) #_"com.whtwnd.blog.entry"
124124+ :theme "github-light"
125125+ :title title
126126+ :content content
127127+ :createdAt (stringify-tid tid)
128128+ :visibility visibility})
129129+130130+(defn blog-ref-record [path]
131131+ {:$type (:m1 collection-nsids) #_"dev.m1emi1em.blog.postRef" ;; Original the NSID do not steal
132132+ :path path})
133133+134134+(defn records-from-file
135135+ "Creates records for both whitewind and also for our own collection to keep track of which records (blog posts) under whitewind were added and are being managed by this script"
136136+ [fs-file]
137137+ (let [created (gen-tid)
138138+ fname (fs/file-name fs-file)
139139+ fpath (str fs-file)]
140140+ {:blog-record (blog-record (->> fname (re-seq #"(.*)\.md$") first second)
141141+ (slurp fpath)
142142+ "public"
143143+ created)
144144+ :blog-ref-record (blog-ref-record fpath)
145145+ :rkey created}))
146146+147147+(defn get-tracked-posts [repo]
148148+ (->> (goat-ls-coll repo (:m1 collection-nsids) #_"dev.m1emi1em.blog.postRef")
149149+ (map (juxt (comp :path #(goat-get-record repo (:m1 collection-nsids) %) :rkey)
150150+ :rkey))))
151151+152152+(def folders {:posts "posts"
153153+ :m1-json "json/m1emi1em"
154154+ :ww-json "json/whtwnd"})
155155+156156+(defn get-local-posts
157157+ ([] (get-local-posts (:posts folders)))
158158+ ([path] (->> path fs/list-dir (map str))))
159159+160160+;;; Fiddling below
161161+162162+(defn title-from-fname [fname] (->> fname fs/file fs/file-name (re-seq #"(.*)\.md$") first second))
163163+164164+(defn blog-record-from-file [fname rkey]
165165+ (blog-record (title-from-fname fname) (slurp fname) "public" rkey))
166166+167167+(defn hash-blog-record [{:keys [title content visibility]}]
168168+ (let [raw (str title content visibility)]
169169+ (-> (shell! {:in raw} "md5sum") (str/split #" ") first)))
170170+171171+(defn hash-blog-file [fname]
172172+ (let [title (->> fname fs/file fs/file-name (re-seq #"(.*)\.md$") first second)
173173+ visibility "public"]
174174+ (hash-blog-record {:title title :content (slurp fname) :visibility visibility})))
175175+176176+(defn create-working-folders []
177177+ (let [folders-to-make (select-keys folders [:m1-json :ww-json])]
178178+ (doseq [folder folders-to-make]
179179+ (->> folder second fs/create-dirs))
180180+ #_(map (comp fs/create-dirs second) folders-to-make)))
181181+182182+(defn find-deleted-posts [local pds]
183183+ (if (not (empty? pds))
184184+ (select-keys (into {} pds)
185185+ (set/difference (->> pds (map first) set) (set local)))
186186+ {}))
187187+188188+(defn find-updated-posts [repo local pds]
189189+ (if (empty? pds)
190190+ {}
191191+ (let [local-hashes (zipmap local (map hash-blog-file local))
192192+193193+ remote-hashes (zipmap (map first pds)
194194+ (map (comp hash-blog-record
195195+ (partial goat-get-record repo (:ww collection-nsids))
196196+ second)
197197+ pds))
198198+199199+ rkey-table (into {} pds)]
200200+ (->> local-hashes
201201+ (map (fn [[fname lhash]] [fname lhash (get remote-hashes fname)]))
202202+ (remove (comp nil? last))
203203+ (remove (comp #(apply = %) (juxt second last)))
204204+ (map first)
205205+ (map (juxt identity #(get rkey-table %)))))))
206206+207207+(defn find-new-posts [local pds]
208208+ (set/difference (set local) (if (empty? pds) #{} (->> pds (map first) set))))
209209+210210+;; Actual write functions
211211+212212+(defn create-both-records! [stuff]
213213+ (let [rkey (:rkey stuff)
214214+215215+ brecord (:blog-record stuff)
216216+ blog-json-fname (str (:ww-json folders) "/" rkey ".json")
217217+218218+ rrecord (:blog-ref-record stuff)
219219+ ref-json-fname (str (:m1-json folders) "/" rkey ".json")]
220220+ (do
221221+ (println "Creating blog and ref records with rkey " rkey)
222222+ (->> brecord json/generate-string (spit blog-json-fname))
223223+ (->> rrecord json/generate-string (spit ref-json-fname))
224224+ (goat-create-record rkey ref-json-fname)
225225+ (goat-create-record rkey blog-json-fname))))
226226+227227+(defn update-blog-record! [brecord rkey]
228228+ (let [blog-json-fname (str (:ww-json folders) "/" rkey ".json")]
229229+ (do
230230+ (println "Updating blogpost " (:title brecord) " at rkey" rkey)
231231+ (->> brecord json/generate-string (spit blog-json-fname))
232232+ (goat-update-record rkey blog-json-fname))))
233233+234234+(defn find-and-do-updates! [local pds]
235235+ (let [to-update (find-updated-posts "m1emi1em.dev" local pds)]
236236+ (doseq [[fname rkey] to-update]
237237+ (let [blog-json-fname (str (:ww-json folders) "/" rkey ".json")]
238238+ (update-blog-record! (blog-record-from-file fname rkey) rkey)))))
239239+240240+(defn do-deletions! [rkey]
241241+ (doseq [c (vals collection-nsids)]
242242+ (println "Deleting rkey " rkey " in " c)
243243+ (goat-delete-record c rkey)))
244244+245245+(defn run []
246246+ (let [local (get-local-posts)
247247+ remote (get-tracked-posts "m1emi1em.dev")
248248+ created (find-new-posts local remote)]
249249+ ;; ]
250250+ (do
251251+ (create-working-folders)
252252+ (println "Checking for new posts")
253253+254254+ (doseq [c created] (-> c records-from-file create-both-records!))
255255+256256+ (when (not (empty? remote))
257257+ (let [deleted (find-deleted-posts local remote)]
258258+ (do
259259+ (println "Checking for updated posts")
260260+ (find-and-do-updates! local remote)
261261+262262+ (println "Checking for deleted posts")
263263+ (doseq [d (vals deleted)] (do-deletions! d)))))
264264+265265+ (println "All done!"))))
266266+267267+(defn -main [& args]
268268+ (do (goat-login) (run)))
269269+270270+(when (= *file* (System/getProperty "babashka.file"))
271271+ (apply -main *command-line-args*))
272272+