;; amycatgirl.github.io no-js fallback ;; Last authored: Thu 19 Mar 18:41:03 AST 2026 ;; Depends on: drakma, yason, uiop, cl-ppcre, trivia, alexandria, spinneret, local-time (ql:quickload '(drakma yason uiop cl-ppcre alexandria spinneret local-time)) (defconstant +user-did+ "did:plc:gijpvbkdbr56kazbdjhfvb3d" "DID of the user to fetch entries from") (defconstant +viewers+ '(("3mi2fpvnluk2b" . "https://amybunny.offprint.app~A") ("3mbpuyp4pqk2j" . "https://amybunny.leaflet.pub/~A")) "An alist of publication keys -> URLs. ~A is passed to `format' with the `path' of the record.") (defconstant +max-entries+ 5 "Maximum amount of entries fetched.") ;; utilities (defmacro run-command (command) `(uiop:run-program ,command :output '(:string :stripped t))) (defun keywordize (name) (intern (string-upcase name) "KEYWORD")) (defmacro get-in (place indicators &key (accessor-fn 'getf)) (let ((rev-indicators (reverse indicators))) (labels ((construct (y ys) (cond ((null ys) (list accessor-fn place y)) (t (list accessor-fn (construct (car ys) (cdr ys)) y))))) (construct (car rev-indicators) (cdr rev-indicators))))) (defun parse-json-lines (file) "Parse a jsonl file. `DOCUMENT' here refers to the file being parsed. See https://jsonlines.org/ for spec." (loop for line from (uiop:split-string file :separator '(\#newline)) collecting (yason:parse line :object-as :plist :object-key-fn #'keywordize))) ;; Git (defun git-add (&rest files) (run-command `("git" "add" ,@files))) (defun git-commit (message) (run-command `("git" "commit" "-m" ,(concatenate 'string "[fallback-gen] " message)))) (defun git-push () (run-command '("git" "push"))) ;; HTTP (defun build-query-params-from-plist (parameters) "Build a string of URL-encoded query parameters from a plist." ;; HACK: ~: consumes the argument, so we need to pass it twice for iteration to work (let ((params-cleaned (mapcar #'(lambda (value) (if (keywordp value) (string-downcase (symbol-name value)) value)) parameters))) (format nil "~:[~;?~{~(~A~)=~A~^&~}~]" parameters params-cleaned))) (defun make-request (where) (let ((stream (drakma:http-request where :want-stream t :user-agent " amycatgirl.github.io/1.0"))) (setf (flexi-streams:flexi-stream-external-format stream) :utf-8) (yason:parse stream :object-as :plist :object-key-fn #'keywordize))) ;; ATProto ;;; DID (defun resolve-did-document--plc (did) (make-request (format nil "https://plc.directory/~a" did))) (defun did->https (did) "Convert `did' to a proper http form." (let* ((method (get-did-method did)) (loq (subseq did (length (format nil "did:~A:" method))))) (concatenate 'string "https://" loq))) (defun resolve-did-document--web (did) "Resolve a DID document via DID:WEB method. See https://w3c-ccg.github.io/did-method-web/ for specification details." (let* ((qualified-path (concatenate 'string (did->https did) "/.well-known/did.json")) (document (make-request qualified-path))) (if (equal (getf document :id) did) document (error "Could not resolve document from did:web ~S" did)))) (defun webvh-get-latest-document-unsafe (log) "Get the latest DID document from the provided webvh `LOG'. Does not ensure integrity and does not process the entire log in order." (getf (last log) :state)) (defun resolve-did-document--webvh (did) "Resolve a DID document via did:webvh method. This is not up to spec, it does not check whether the encoded path is valid. See https://identity.foundation/didwebvh/v1.0/#read-resolve for spec details." (let* ((qualified-path (concatenate 'string (did->https did) "/.well-known/did.jsonl")) (log-json-lines (make-request qualified-path)) (log (parse-json-lines log-json-lines))) (webvh-get-latest-document-unsafe log))) (defun get-did-method (did) (cl-ppcre:register-groups-bind (method) ("^did:([a-z]+):[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$" did) method)) (defun resolve-pds (did) "Resolve a PDS by DID." (let* ((did-method (get-did-method did)) (document (case did-method ("web" (resolve-did-document--web did)) ("plc" (resolve-did-document--plc did)) ("webvh" (resolve-did-document--webvh did)) (_ (error "Unsuported DID method ~S" did-method)))) (services (getf document :service))) (getf (find-if (lambda (service) (equal (getf service :id) "#atproto_pds")) services) :serviceEndpoint))) ;;; PDS (defun make-pds-request (pds method &rest query) "Make a unauthenticated XRPC call to the PDS, where `method' is the NSID of the endpoint to call. Aditional query parameters in the request must be passed inside of `query', where key-value pairs are denoted by keywords and values." (let ((url (format nil "~A/xrpc/~A~A" pds method (build-query-params-from-plist query)))) (make-request url))) (defun fetch-entries (did &optional (maximum 5)) (let* ((pds (resolve-pds did))) (make-pds-request pds "com.atproto.repo.listRecords" :repo did :collection "site.standard.document" :limit maximum))) ;; Standard.site (defun get-publication-rkey (site) (car (last (uiop:split-string site :separator '(#\/))))) ;; Generator (defun published-at-element (date) (let ((time (local-time:parse-timestring date))) (spinneret:with-html (:time.published-at :attrs (list :datetime date) (local-time:format-timestring nil time :format '((:day 2) "/" (:month 2) "/" :year ", " (:hour 2) #\: (:min 2))))))) (defun entry-element (entry) (let ((title (get-in entry (:value :title))) (description (or (get-in entry (:value :description)) "")) (date (get-in entry (:value :publishedat))) (link (format nil (cdr (assoc (get-publication-rkey (get-in entry (:value :site))) +viewers+ :test 'equal)) (get-in entry (:value :path)))) (spinneret:*html-style* :tree)) (spinneret:with-html-string (:div.entry (:a.base-anchor :href link (:h3.title title)) (:p.description description) (:div.metadata (published-at-element date)))))) (defun generate-html (entries) (format nil "~%~{~A~^~%~}~%" (mapcar #'entry-element entries))) (defun read-file-to-string (path) (with-open-file (stream path :direction :input :if-does-not-exist :error) (let ((buf (make-string (file-length stream)))) (read-sequence buf stream) buf))) (defun write-to-noscript-block (page entries) (let* ((fragment (generate-html entries)) (file-content (read-file-to-string page)) (start-pos (or (search "" file-content) (error "Could not find closing tag in ~A" page))) (insert-start (+ start-pos (length "