112 conversions across 19 .sx files using match, let-match, and pipe operators: match (17): type/value dispatch replacing cond/if chains - lib/vm.sx: HO form dispatch (for-each/map/filter/reduce/some/every?) - lib/tree-tools.sx: node-display, node-matches?, rename, count, replace, free-symbols - lib/types.sx: narrow-type, substitute-in-type, infer-type, resolve-type - web/engine.sx: default-trigger, resolve-target, classify-trigger - web/deps.sx: scan-refs-walk, scan-io-refs-walk let-match (89): dict destructuring replacing (get d "key") patterns - shared/page-functions.sx (20), blog/admin.sx (17), pub-api.sx (13) - events/ layouts/page/tickets/entries/forms (27 total) - specs-explorer.sx (7), federation/social.sx (3), lib/ small files (3) -> pipes (6): replacing triple-chained gets in lib/vm.sx - frame-closure → closure-code → code-bytecode chains Also: lib/vm.sx accessor upgrades (get vm "sp" → vm-sp vm throughout) 2650/2650 tests pass, zero regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
501 lines
15 KiB
Plaintext
501 lines
15 KiB
Plaintext
;; ==========================================================================
|
|
;; sx-pub API endpoints — actor, webfinger, collections, publishing, browsing
|
|
;;
|
|
;; All responses are text/sx. No JSON.
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Actor
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-actor
|
|
:path "/pub/actor"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let-match
|
|
{:domain domain :summary summary :display-name display-name :public-key-pem public-key-pem}
|
|
(helper "pub-actor-data")
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str
|
|
"(SxActor"
|
|
"\n :id \"https://"
|
|
domain
|
|
"/pub/actor\""
|
|
"\n :type \"SxPublisher\""
|
|
"\n :name \""
|
|
display-name
|
|
"\""
|
|
"\n :summary \""
|
|
summary
|
|
"\""
|
|
"\n :inbox \"/pub/inbox\""
|
|
"\n :outbox \"/pub/outbox\""
|
|
"\n :followers \"/pub/followers\""
|
|
"\n :following \"/pub/following\""
|
|
"\n :public-key-pem \""
|
|
public-key-pem
|
|
"\")"))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Webfinger
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-webfinger
|
|
:path "/pub/webfinger"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let
|
|
((resource (helper "request-arg" "resource" "")))
|
|
(let-match
|
|
{:domain domain :preferred-username preferred-username}
|
|
(helper "pub-actor-data")
|
|
(let
|
|
((expected (str "acct:" preferred-username "@" domain)))
|
|
(if
|
|
(!= resource expected)
|
|
(do
|
|
(set-response-status 404)
|
|
(str "(Error :message \"Resource not found\")"))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str
|
|
"(SxWebfinger"
|
|
"\n :subject \""
|
|
expected
|
|
"\""
|
|
"\n :actor \"https://"
|
|
domain
|
|
"/pub/actor\""
|
|
"\n :type \"SxPublisher\")")))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Collections
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-collections
|
|
:path "/pub/collections"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let
|
|
((collections (helper "pub-collections-data")))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(let
|
|
((items (map (fn (c) (let-match {:description description :slug slug :name name} c (str "\n (SxCollection" " :slug \"" slug "\"" " :name \"" name "\"" " :description \"" description "\"" " :href \"/pub/" slug "\")"))) collections)))
|
|
(str "(SxCollections" (join "" items) ")")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Status
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-status
|
|
:path "/pub/status"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let-match
|
|
{:db db :domain domain :healthy healthy :ipfs ipfs :actor actor}
|
|
(helper "pub-status-data")
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str
|
|
"(SxPubStatus"
|
|
"\n :healthy "
|
|
healthy
|
|
"\n :db \""
|
|
db
|
|
"\""
|
|
"\n :ipfs \""
|
|
ipfs
|
|
"\""
|
|
"\n :actor \""
|
|
actor
|
|
"\""
|
|
"\n :domain \""
|
|
(or domain "unknown")
|
|
"\")"))))
|
|
|
|
|
|
;; ==========================================================================
|
|
;; Phase 2: Publishing + Browsing
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Publish
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-publish
|
|
:path "/pub/publish"
|
|
:method :post
|
|
:csrf false
|
|
:returns "element"
|
|
(&key)
|
|
(let
|
|
((collection (helper "request-form" "collection" ""))
|
|
(slug (helper "request-form" "slug" ""))
|
|
(content (helper "request-form" "content" ""))
|
|
(title (helper "request-form" "title" ""))
|
|
(summary (helper "request-form" "summary" "")))
|
|
(if
|
|
(or (= collection "") (= slug "") (= content ""))
|
|
(do
|
|
(set-response-status 400)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
"(Error :message \"Missing collection, slug, or content\")")
|
|
(let
|
|
((result (helper "pub-publish" collection slug content title summary)))
|
|
(if
|
|
(get result "error")
|
|
(do
|
|
(set-response-status 500)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str "(Error :message \"" (get result "error") "\")"))
|
|
(let-match
|
|
{:cid cid :hash hash :size size :title title :path path :slug slug :collection collection}
|
|
result
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str
|
|
"(Published"
|
|
"\n :path \""
|
|
path
|
|
"\""
|
|
"\n :cid \""
|
|
cid
|
|
"\""
|
|
"\n :hash \""
|
|
hash
|
|
"\""
|
|
"\n :size "
|
|
size
|
|
"\n :collection \""
|
|
collection
|
|
"\""
|
|
"\n :slug \""
|
|
slug
|
|
"\""
|
|
"\n :title \""
|
|
title
|
|
"\")"))))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Browse collection
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-browse-collection
|
|
:path "/pub/browse/<collection_slug>"
|
|
:method :get
|
|
:returns "element"
|
|
(&key collection_slug)
|
|
(let
|
|
((data (helper "pub-collection-items" collection_slug)))
|
|
(if
|
|
(get data "error")
|
|
(do
|
|
(set-response-status 404)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str "(Error :message \"" (get data "error") "\")"))
|
|
(let-match
|
|
{:description description :items items-data :collection collection :name name}
|
|
data
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(let
|
|
((items (map (fn (d) (let-match {:cid cid :size size :summary summary :title title :slug slug} d (str "\n (SxDocument" " :slug \"" slug "\"" " :title \"" title "\"" " :summary \"" summary "\"" " :cid \"" cid "\"" " :size " size ")"))) items-data)))
|
|
(str
|
|
"(SxCollection"
|
|
"\n :slug \""
|
|
collection
|
|
"\""
|
|
"\n :name \""
|
|
name
|
|
"\""
|
|
"\n :description \""
|
|
description
|
|
"\""
|
|
(join "" items)
|
|
")")))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Resolve document by path
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler pub-document
|
|
:path "/pub/doc/<collection_slug>/<slug>"
|
|
:method :get
|
|
:returns "element"
|
|
(&key collection_slug slug)
|
|
(let ((data (helper "pub-resolve-document" collection_slug slug)))
|
|
(if (get data "error")
|
|
(do
|
|
(set-response-status 404)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str "(Error :message \"" (get data "error") "\")"))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(set-response-header "X-IPFS-CID" (get data "cid"))
|
|
(get data "content")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Direct CID fetch
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler pub-cid
|
|
:path "/pub/cid/<cid>"
|
|
:method :get
|
|
:returns "element"
|
|
(&key cid)
|
|
(let ((data (helper "pub-fetch-cid" cid)))
|
|
(if (get data "error")
|
|
(do
|
|
(set-response-status 404)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str "(Error :message \"" (get data "error") "\")"))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(set-response-header "Cache-Control" "public, max-age=31536000, immutable")
|
|
(get data "content")))))
|
|
|
|
|
|
;; ==========================================================================
|
|
;; Phase 3: Federation — outbox, inbox, follow, followers, following
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Outbox
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-outbox
|
|
:path "/pub/outbox"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let
|
|
((page (helper "request-arg" "page" ""))
|
|
(data (helper "pub-outbox-data" page)))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(let-match
|
|
{:total total :page page :items items-data}
|
|
data
|
|
(let
|
|
((items (map (fn (a) (let-match {:cid cid :type type :object-type object-type :published published} a (str "\n (" type " :object-type \"" object-type "\"" " :published \"" published "\"" " :cid \"" cid "\")"))) items-data)))
|
|
(str
|
|
"(SxOutbox"
|
|
"\n :total "
|
|
total
|
|
"\n :page "
|
|
page
|
|
(join "" items)
|
|
")"))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Inbox
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler pub-inbox
|
|
:path "/pub/inbox"
|
|
:method :post
|
|
:csrf false
|
|
:returns "element"
|
|
(&key)
|
|
(let ((body (helper "pub-request-body")))
|
|
(if (= body "")
|
|
(do
|
|
(set-response-status 400)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
"(Error :message \"Empty body\")")
|
|
(let ((result (helper "pub-process-inbox" body)))
|
|
(do
|
|
(set-response-status 202)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(str "(Accepted :result " (str result) ")"))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Follow a remote server
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-follow
|
|
:path "/pub/follow"
|
|
:method :post
|
|
:csrf false
|
|
:returns "element"
|
|
(&key)
|
|
(let
|
|
((actor-url (helper "request-form" "actor_url" "")))
|
|
(if
|
|
(= actor-url "")
|
|
(do
|
|
(set-response-status 400)
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
"(Error :message \"Missing actor_url\")")
|
|
(let
|
|
((result (helper "pub-follow-remote" actor-url)))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(if
|
|
(get result "error")
|
|
(do
|
|
(set-response-status 502)
|
|
(str "(Error :message \"" (get result "error") "\")"))
|
|
(let-match
|
|
{:status status :actor-url actor-url}
|
|
result
|
|
(str
|
|
"(FollowSent"
|
|
"\n :actor-url \""
|
|
actor-url
|
|
"\""
|
|
"\n :status \""
|
|
status
|
|
"\")"))))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Followers
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-followers
|
|
:path "/pub/followers"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let
|
|
((data (helper "pub-followers-data")))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(let
|
|
((items (map (fn (f) (let-match {:actor-url actor-url :acct acct} f (str "\n (SxFollower" " :acct \"" acct "\"" " :actor-url \"" actor-url "\")"))) data)))
|
|
(str "(SxFollowers" (join "" items) ")")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Following
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler pub-following
|
|
:path "/pub/following"
|
|
:method :get
|
|
:returns "element"
|
|
(&key)
|
|
(let ((data (helper "pub-following-data")))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(let ((items (map (fn (f)
|
|
(str "\n (SxFollowing"
|
|
" :actor-url \"" (get f "actor-url") "\")"))
|
|
data)))
|
|
(str "(SxFollowing" (join "" items) ")")))))
|
|
|
|
|
|
;; ==========================================================================
|
|
;; Phase 4: Anchoring — Merkle trees, OTS, verification
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Anchor pending activities
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-anchor
|
|
:path "/pub/anchor"
|
|
:method :post
|
|
:csrf false
|
|
:returns "element"
|
|
(&key)
|
|
(let-match
|
|
{:tree-cid tree-cid :status status :count count :ots-proof-cid ots-proof-cid :merkle-root merkle-root}
|
|
(helper "pub-anchor-pending")
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(if
|
|
(= status "nothing-to-anchor")
|
|
"(Anchor :status \"nothing-to-anchor\" :count 0)"
|
|
(str
|
|
"(Anchor"
|
|
"\n :status \""
|
|
status
|
|
"\""
|
|
"\n :count "
|
|
count
|
|
"\n :merkle-root \""
|
|
merkle-root
|
|
"\""
|
|
"\n :tree-cid \""
|
|
tree-cid
|
|
"\""
|
|
"\n :ots-proof-cid \""
|
|
ots-proof-cid
|
|
"\")")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Verify a CID's anchor
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defhandler
|
|
pub-verify
|
|
:path "/pub/verify/<cid>"
|
|
:method :get
|
|
:returns "element"
|
|
(&key cid)
|
|
(let
|
|
((data (helper "pub-verify-anchor" cid)))
|
|
(do
|
|
(set-response-header "Content-Type" "text/sx; charset=utf-8")
|
|
(if
|
|
(get data "error")
|
|
(do
|
|
(set-response-status 404)
|
|
(str "(Error :message \"" (get data "error") "\")"))
|
|
(let-match
|
|
{:cid cid* :tree-cid tree-cid :status status :verified verified :ots-proof-cid ots-proof-cid :merkle-root merkle-root :published published}
|
|
data
|
|
(str
|
|
"(AnchorVerification"
|
|
"\n :cid \""
|
|
cid*
|
|
"\""
|
|
"\n :status \""
|
|
status
|
|
"\""
|
|
"\n :verified "
|
|
verified
|
|
"\n :merkle-root \""
|
|
merkle-root
|
|
"\""
|
|
"\n :tree-cid \""
|
|
tree-cid
|
|
"\""
|
|
"\n :ots-proof-cid \""
|
|
ots-proof-cid
|
|
"\""
|
|
"\n :published \""
|
|
published
|
|
"\")"))))))
|