at main 9.5 kB view raw
1#!/usr/bin/env bb 2(require '[babashka.curl :as curl] 3 '[babashka.fs :as fs] 4 '[babashka.process :refer [shell]] 5 '[cheshire.core :as json] 6 '[clojure.string :as str]) 7 8;; (def repo-details {:collection "com.whtwnd.blog.entry"}) 9(def collection-nsids {:ww "com.whtwnd.blog.entry" 10 :m1 "dev.m1emi1em.blog.postRef"}) 11 12(def default-shell!!-args {:out :string}) 13 14(defn shell! 15 "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" 16 [cmd-or-opts & args] 17 (let [defaults? (not (instance? clojure.lang.PersistentArrayMap cmd-or-opts)) 18 opts (cond-> default-shell!!-args 19 (not defaults?) (merge cmd-or-opts)) 20 args (cond->> args 21 defaults? (cons cmd-or-opts))] 22 (:out (apply shell opts args)))) 23;; goat helpers 24 25(defn goat-login 26 "Username and password are picked up from the ATP_AUTH_USERNAME and ATP_AUTH_PASSWORD env vars" 27 [] (shell! "goat account login")) 28 29(defn goat-ls-coll [repo collection] 30 (->> (shell! "goat ls --collection" collection repo) 31 str/split-lines 32 (map #(->> (str/split % #"\t") 33 rest 34 (zipmap [:rkey :cid]))))) 35 36(defn goat-get-record [repo collection rkey] 37 (do (println "Getting record " rkey " from collection " collection " in " repo) 38 (if (nil? rkey) 39 {} 40 (-> (shell! "goat record get" 41 (str "at://" repo "/" collection "/" rkey)) 42 json/parse-string 43 (update-keys keyword)) 44 ))) 45 46(defn goat-create-record [rkey fname] 47 (do (println "Creating record " rkey " from " fname) 48 (shell! "goat record create -r" rkey "-n" fname))) 49 50(defn goat-update-record [rkey fname] 51 (do (println "Updating record " rkey " from " fname) 52 (shell! "goat record update -r" rkey "-n" fname))) 53 54(defn goat-delete-record [collection rkey] 55 (do (println "Deleting record " rkey " from " collection)) 56 (if (nil? rkey) 57 nil 58 (shell! (str "goat record delete -c " collection " -r " rkey)))) 59 60;; TID Stuff 61;; Loosely adapted from https://github.com/BlushSocial/atproto-tid 62;; Not written with concurrency in mind 63;; Or error handling (what can possibly go wrong?) 64 65(defn gen-timestamp [] 66 ;; Not actually sure why we multiply this here but rolling with it 67 (->> (java.util.Date.) .getTime (* 1000))) 68 69(def s32-char (vec "234567abcdefghijklmnopqrstuvwxyz")) 70 71(defn base32 [n padding] 72 (let [pieces (->> n 73 (iterate #(quot % 32)) 74 (take-while #(> % 0)) 75 (map #(mod % 32)))] 76 (as-> pieces $ 77 (map (partial nth s32-char) $) 78 (reverse $) 79 (concat $ (repeat \2)) 80 (take padding $) 81 (apply str $)))) 82 83;; I think this would be more readable but I'm too lazy to make sure it works rn 84;; (-> pieces 85;; (->> (map #(nth s32-char %)) reverse) 86;; (concat (repeat \2)) 87;; (->> (take padding) (apply str)) 88 89(defn unbase32 [tid] 90 (let [table (->> s32-char (map-indexed #(vector %2 %1)) (into {})) 91 lookup #(get table %)] 92 (->> tid 93 (map lookup) 94 (reduce #(+ (* %1 32) %2) 0)))) 95 96(defn gen-tid 97 ([] (gen-tid (gen-timestamp) (rand-int 1023))) 98 ([timestamp clockid] 99 (str (base32 timestamp 11) (base32 clockid 2)))) 100 101(defn parse-tid [tid] 102 {:timestamp (unbase32 (subs tid 0 11)) 103 :clockid (unbase32 (subs tid 11 13))}) 104 105(defn stringify-tid 106 "I'm just gonna use date for this lol" 107 [tid] 108 (let [{:keys [timestamp clockid]} (parse-tid tid) 109 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 110 date-format-str "+%Y-%m-%dT%H:%M:%S" 111 112 time-string 113 (-> (shell {:out :string} 114 "date -d" 115 (str "@" unix-timestamp) 116 date-format-str) 117 :out str/trim)] 118 (format "%s.%03dZ" time-string clockid))) 119 120;;; 121 122(defn blog-record [title content visibility tid] 123 {:$type (:ww collection-nsids) #_"com.whtwnd.blog.entry" 124 :theme "github-light" 125 :title title 126 :content content 127 :createdAt (stringify-tid tid) 128 :visibility visibility}) 129 130(defn blog-ref-record [path] 131 {:$type (:m1 collection-nsids) #_"dev.m1emi1em.blog.postRef" ;; Original the NSID do not steal 132 :path path}) 133 134(defn records-from-file 135 "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" 136 [fs-file] 137 (let [created (gen-tid) 138 fname (fs/file-name fs-file) 139 fpath (str fs-file)] 140 {:blog-record (blog-record (->> fname (re-seq #"(.*)\.md$") first second) 141 (slurp fpath) 142 "public" 143 created) 144 :blog-ref-record (blog-ref-record fpath) 145 :rkey created})) 146 147(defn get-tracked-posts [repo] 148 (->> (goat-ls-coll repo (:m1 collection-nsids) #_"dev.m1emi1em.blog.postRef") 149 (map (juxt (comp :path #(goat-get-record repo (:m1 collection-nsids) %) :rkey) 150 :rkey)))) 151 152(def folders {:posts "posts" 153 :m1-json "json/m1emi1em" 154 :ww-json "json/whtwnd"}) 155 156(defn get-local-posts 157 ([] (get-local-posts (:posts folders))) 158 ([path] (->> path fs/list-dir (map str)))) 159 160;;; Fiddling below 161 162(defn title-from-fname [fname] (->> fname fs/file fs/file-name (re-seq #"(.*)\.md$") first second)) 163 164(defn blog-record-from-file [fname rkey] 165 (blog-record (title-from-fname fname) (slurp fname) "public" rkey)) 166 167(defn hash-blog-record [{:keys [title content visibility]}] 168 (let [raw (str title content visibility)] 169 (-> (shell! {:in raw} "md5sum") (str/split #" ") first))) 170 171(defn hash-blog-file [fname] 172 (let [title (->> fname fs/file fs/file-name (re-seq #"(.*)\.md$") first second) 173 visibility "public"] 174 (hash-blog-record {:title title :content (slurp fname) :visibility visibility}))) 175 176(defn create-working-folders [] 177 (let [folders-to-make (select-keys folders [:m1-json :ww-json])] 178 (doseq [folder folders-to-make] 179 (->> folder second fs/create-dirs)) 180 #_(map (comp fs/create-dirs second) folders-to-make))) 181 182(defn find-deleted-posts [local pds] 183 (if (not (empty? pds)) 184 (select-keys (into {} pds) 185 (set/difference (->> pds (map first) set) (set local))) 186 {})) 187 188(defn find-updated-posts [repo local pds] 189 (if (empty? pds) 190 {} 191 (let [local-hashes (zipmap local (map hash-blog-file local)) 192 193 remote-hashes (zipmap (map first pds) 194 (map (comp hash-blog-record 195 (partial goat-get-record repo (:ww collection-nsids)) 196 second) 197 pds)) 198 199 rkey-table (into {} pds)] 200 (->> local-hashes 201 (map (fn [[fname lhash]] [fname lhash (get remote-hashes fname)])) 202 (remove (comp nil? last)) 203 (remove (comp #(apply = %) (juxt second last))) 204 (map first) 205 (map (juxt identity #(get rkey-table %))))))) 206 207(defn find-new-posts [local pds] 208 (set/difference (set local) (if (empty? pds) #{} (->> pds (map first) set)))) 209 210;; Actual write functions 211 212(defn create-both-records! [stuff] 213 (let [rkey (:rkey stuff) 214 215 brecord (:blog-record stuff) 216 blog-json-fname (str (:ww-json folders) "/" rkey ".json") 217 218 rrecord (:blog-ref-record stuff) 219 ref-json-fname (str (:m1-json folders) "/" rkey ".json")] 220 (do 221 (println "Creating blog and ref records with rkey " rkey) 222 (->> brecord json/generate-string (spit blog-json-fname)) 223 (->> rrecord json/generate-string (spit ref-json-fname)) 224 (goat-create-record rkey ref-json-fname) 225 (goat-create-record rkey blog-json-fname)))) 226 227(defn update-blog-record! [brecord rkey] 228 (let [blog-json-fname (str (:ww-json folders) "/" rkey ".json")] 229 (do 230 (println "Updating blogpost " (:title brecord) " at rkey" rkey) 231 (->> brecord json/generate-string (spit blog-json-fname)) 232 (goat-update-record rkey blog-json-fname)))) 233 234(defn find-and-do-updates! [local pds] 235 (let [to-update (find-updated-posts "m1emi1em.dev" local pds)] 236 (doseq [[fname rkey] to-update] 237 (let [blog-json-fname (str (:ww-json folders) "/" rkey ".json")] 238 (update-blog-record! (blog-record-from-file fname rkey) rkey))))) 239 240(defn do-deletions! [rkey] 241 (doseq [c (vals collection-nsids)] 242 (println "Deleting rkey " rkey " in " c) 243 (goat-delete-record c rkey))) 244 245(defn run [] 246 (let [local (get-local-posts) 247 remote (get-tracked-posts "m1emi1em.dev") 248 created (find-new-posts local remote)] 249 ;; ] 250 (do 251 (create-working-folders) 252 (println "Checking for new posts") 253 254 (doseq [c created] (-> c records-from-file create-both-records!)) 255 256 (when (not (empty? remote)) 257 (let [deleted (find-deleted-posts local remote)] 258 (do 259 (println "Checking for updated posts") 260 (find-and-do-updates! local remote) 261 262 (println "Checking for deleted posts") 263 (doseq [d (vals deleted)] (do-deletions! d))))) 264 265 (println "All done!")))) 266 267(defn -main [& args] 268 (do (goat-login) (run))) 269 270(when (= *file* (System/getProperty "babashka.file")) 271 (apply -main *command-line-args*)) 272