Files
rose-ash/sx/sx/handlers/pub-api.sx
giles c0665ba58e Adopt Step 7 language features across SX codebase
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>
2026-04-04 20:49:02 +00:00

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
"\")"))))))