diff --git a/README.md b/README.md new file mode 100644 index 0000000..bee1748 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Souk - ActivityPub in Clojure + +A work-in-progress implementation of the ActivityPub standard in Clojure. + +This repo should primarily be understood as a byproduct of the live streams +which explore this topic, it is not meant to be generally consumable, either as +a library or an application. + diff --git a/deps.edn b/deps.edn index 17044ea..c603c31 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,28 @@ -{:deps {hato/hato {:mvn/version "0.9.0"} - cheshire/cheshire {:mvn/version "5.11.0"} - seancorfield/next.jdbc {:mvn/version "1.2.659"} - com.impossibl.pgjdbc-ng/pgjdbc-ng {:mvn/version "0.8.9"} +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.11.1"} - }} + ;; Application setup + com.lambdaisland/webbing {:local/root "/home/arne/github/lambdaisland/webbing"} +;; {:mvn/version "0.4.20-alpha"} + ;; Incoming HTTP + ring/ring-core {:mvn/version "1.9.6"} + ring/ring-jetty-adapter {:mvn/version "1.9.6"} + ring/ring-mock {:mvn/version "0.4.0"} + metosin/malli {:mvn/version "0.9.2"} + metosin/muuntaja {:mvn/version "0.6.8"} + metosin/reitit {:mvn/version "0.5.18"} + + ;; Outgoing HTTP + hato/hato {:mvn/version "0.9.0"} + + ;; Formats + cheshire/cheshire {:mvn/version "5.11.0"} + + ;; Database + seancorfield/next.jdbc {:mvn/version "1.2.659"} + com.impossibl.pgjdbc-ng/pgjdbc-ng {:mvn/version "0.8.9"} + } + + :aliases + {:dev {:extra-paths ["dev"]} + :souk {:main-opts ["-m" "lambdaisland.souk"]}}} diff --git a/dev/user.clj b/dev/user.clj new file mode 100644 index 0000000..cae03eb --- /dev/null +++ b/dev/user.clj @@ -0,0 +1,3 @@ +(ns user) + +(set! *print-namespace-maps* false) diff --git a/preview-image.png b/preview-image.png new file mode 100644 index 0000000..f426213 Binary files /dev/null and b/preview-image.png differ diff --git a/preview-image.svg b/preview-image.svg new file mode 100644 index 0000000..2f14c5c --- /dev/null +++ b/preview-image.svg @@ -0,0 +1,5507 @@ + + + ++Episode 1 +JSON-LD to EDN diff --git a/repl_sessions/vocabulary.clj b/repl_sessions/vocabulary.clj index 21fe38e..4d72138 100644 --- a/repl_sessions/vocabulary.clj +++ b/repl_sessions/vocabulary.clj @@ -2,7 +2,6 @@ (:require [lambdaisland.souk.json-ld :as ld] [lambdaisland.souk.activitypub :refer :all])) - (ld/expand (:body (ld/json-get "https://raw.githubusercontent.com/gobengo/activitystreams2-spec-scraped/master/data/activitystreams-vocabulary/1528589057.json"))) (let [{:owl/keys [imports]} (GET "https://raw.githubusercontent.com/gobengo/activitystreams2-spec-scraped/master/data/activitystreams-vocabulary/1528589057.json")] diff --git a/resources/lambdaisland/souk/config.edn b/resources/lambdaisland/souk/config.edn new file mode 100644 index 0000000..a6df58e --- /dev/null +++ b/resources/lambdaisland/souk/config.edn @@ -0,0 +1,8 @@ +{:http/router + {:gx/component lambdaisland.souk.components.router/component + :gx/props {:dev-router? #setting :dev/reload-routes?}} + + :http/server + {:gx/component lambdaisland.souk.components.jetty/component + :gx/props {:jetty-options {:port #setting :port} + :router (gx/ref :http/router)}}} diff --git a/resources/lambdaisland/souk/settings-dev.edn b/resources/lambdaisland/souk/settings-dev.edn new file mode 100644 index 0000000..a1b11d6 --- /dev/null +++ b/resources/lambdaisland/souk/settings-dev.edn @@ -0,0 +1 @@ +{:dev/reload-routes? true} diff --git a/resources/lambdaisland/souk/settings-prod.edn b/resources/lambdaisland/souk/settings-prod.edn new file mode 100644 index 0000000..09c9d3f --- /dev/null +++ b/resources/lambdaisland/souk/settings-prod.edn @@ -0,0 +1 @@ +{:port 80} diff --git a/resources/lambdaisland/souk/settings.edn b/resources/lambdaisland/souk/settings.edn new file mode 100644 index 0000000..6a7e3a5 --- /dev/null +++ b/resources/lambdaisland/souk/settings.edn @@ -0,0 +1,2 @@ +{:port 3444 + :dev/reload-routes? false} diff --git a/src/lambdaisland/souk.clj b/src/lambdaisland/souk.clj new file mode 100644 index 0000000..ba85b1e --- /dev/null +++ b/src/lambdaisland/souk.clj @@ -0,0 +1,6 @@ +(ns lambdaisland.souk + (:require [lambdaisland.webbing.prod :as prod] + [lambdaisland.souk.setup :as setup])) + +(defn -main [& _] + (prod/go (setup/prod-setup))) diff --git a/src/lambdaisland/souk/activitypub.clj b/src/lambdaisland/souk/activitypub.clj index f16b297..44b6430 100644 --- a/src/lambdaisland/souk/activitypub.clj +++ b/src/lambdaisland/souk/activitypub.clj @@ -1,8 +1,7 @@ (ns lambdaisland.souk.activitypub + "Interact with ActivityPub instances" (:require [lambdaisland.souk.json-ld :as ld])) -(set! *print-namespace-maps* false) - (def common-prefixes {"dcterms" "http://purl.org/dc/terms/" "ldp" "http://www.w3.org/ns/ldp#" diff --git a/src/lambdaisland/souk/components/jetty.clj b/src/lambdaisland/souk/components/jetty.clj new file mode 100644 index 0000000..9075c99 --- /dev/null +++ b/src/lambdaisland/souk/components/jetty.clj @@ -0,0 +1,93 @@ +(ns lambdaisland.souk.components.jetty + (:require [ring.adapter.jetty :as ring.jetty] + [reitit.ring :as reitit-ring]) + (:import (org.eclipse.jetty.server Server))) + +(def defaults {:join? false}) + +(def ?JettyOptions + "Start a Jetty webserver to serve the given handler according to the + supplied options: + :configurator - a function called with the Jetty Server instance + :async? - if true, treat the handler as asynchronous + :async-timeout - async context timeout in ms + (defaults to 0, no timeout) + :async-timeout-handler - an async handler to handle an async context timeout + :port - the port to listen on (defaults to 80) + :host - the hostname to listen on + :join? - blocks the thread until server ends + (defaults to true) + :daemon? - use daemon threads (defaults to false) + :http? - listen on :port for HTTP traffic (defaults to true) + :ssl? - allow connections over HTTPS + :ssl-port - the SSL port to listen on (defaults to 443, implies) + :ssl? is true + :ssl-context - an optional SSLContext to use for SSL connections + :exclude-ciphers - when :ssl? is true, additionally exclude these + cipher suites + :exclude-protocols - when :ssl? is true, additionally exclude these + protocols + :replace-exclude-ciphers? - when true, :exclude-ciphers will replace rather + than add to the cipher exclusion list (defaults) + to false + :replace-exclude-protocols? - when true, :exclude-protocols will replace + rather than add to the protocols exclusion list + (defaults to false) + :keystore - the keystore to use for SSL connections + :keystore-type - the keystore type (default jks) + :key-password - the password to the keystore + :keystore-scan-interval - if not nil, the interval in seconds to scan for an + updated keystore + :thread-pool - custom thread pool instance for Jetty to use + :truststore - a truststore to use for SSL connections + :trust-password - the password to the truststore + :max-threads - the maximum number of threads to use (default 50) + :min-threads - the minimum number of threads to use (default 8) + :max-queued-requests - the maximum number of requests to be queued + :thread-idle-timeout - Set the maximum thread idle time. Threads that are + idle for longer than this period may be stopped + (default 60000) + :max-idle-time - the maximum idle time in milliseconds for a + connection (default 200000) + :client-auth - SSL client certificate authenticate, may be set to + :need,:want or :none (defaults to :none) + :send-date-header? - add a date header to the response (default true) + :output-buffer-size - the response body buffer size (default 32768) + :request-header-size - the maximum size of a request header (default 8192) + :response-header-size - the maximum size of a response header (default 8192) + :send-server-version? - add Server header to HTTP response (default true)" + [:map + [:port pos-int?] + [:host {:optional true} string?] + [:max-threads {:optional true} pos-int?] + [:min-threads {:optional true} pos-int?] + [:max-queued-requests {:optional true} pos-int?]]) + +(def ?PropsSchema + [:map + [:jetty-options ?JettyOptions]]) + +(defn ring-handler [reitit-router] + (reitit-ring/ring-handler + reitit-router + (reitit-ring/routes + (reitit-ring/create-default-handler)))) + +(defn http-start [{:keys [props]}] + (let [{:keys [router jetty-options]} props + jetty-options (merge defaults jetty-options)] + (ring.jetty/run-jetty (ring-handler router) jetty-options))) + +(defn http-stop [{^Server this :value}] + (.stop this)) + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(def component + {:gx/start {:gx/processor http-start + :gx/props-schema ?PropsSchema} + :gx/stop {:gx/processor http-stop}}) + +(comment + {:gx/component k16.gx.contrib.jetty/component + :gx/props {:jetty-options {} + :handler '(gx/ref :handler)}}) diff --git a/src/lambdaisland/souk/components/router.clj b/src/lambdaisland/souk/components/router.clj new file mode 100644 index 0000000..081a405 --- /dev/null +++ b/src/lambdaisland/souk/components/router.clj @@ -0,0 +1,48 @@ +(ns lambdaisland.souk.components.router + "Reitit routes and router" + (:require + [lambdaisland.souk.dev-router :as dev-router] + [muuntaja.core :as muuntaja] + [reitit.dev.pretty :as pretty] + [reitit.ring :as reitit-ring] + [reitit.ring.coercion :as reitit-coercion] + [reitit.ring.middleware.muuntaja :as reitit-muuntaja] + [reitit.ring.middleware.parameters :as reitit-parameters])) + +(defn routes [opts] + [["/" + {:get + {:handler + (fn [req] + {:status 200 + :body "OK!"})}}]]) + +(defn wrap-request-context [handler ctx] + (fn [req] + (handler + (assoc req :souk/ctx ctx)))) + +(defn create-router + [props] + (reitit.ring/router + (into [] (remove nil? (routes props))) + {:exception pretty/exception + :data {:muuntaja muuntaja/instance + :middleware [;; ↓↓↓ request passes through middleware top-to-bottom ↓↓↓ + reitit-parameters/parameters-middleware + reitit-muuntaja/format-negotiate-middleware + reitit-muuntaja/format-response-middleware + reitit-muuntaja/format-request-middleware + reitit-coercion/coerce-response-middleware + reitit-coercion/coerce-request-middleware + [wrap-request-context props] + ;; ↑↑↑ response passes through middleware bottom-to-top ↑↑↑ + ]}})) + +(defn start! [{:keys [props]}] + (if (:dev-router? props) + (dev-router/dev-router #(create-router props)) + (create-router props))) + +(def component + {:gx/start {:gx/processor start!}}) diff --git a/src/lambdaisland/souk/dev_router.clj b/src/lambdaisland/souk/dev_router.clj new file mode 100644 index 0000000..71fc8b8 --- /dev/null +++ b/src/lambdaisland/souk/dev_router.clj @@ -0,0 +1,19 @@ +(ns lambdaisland.souk.dev-router + "Reitit router wrapper that auto-rebuilds, for REPL workflows." + (:require [reitit.core :as reitit])) + +(defn dev-router + "Given a function which builds a reitit router, returns a router which rebuilds + the router on every call. This makes sure redefining routes in a REPL works as + expected. Should only every be used in development mode, since it completely + undoes all of reitit's great performance." + [new-router] + (reify reitit/Router + (router-name [_] (reitit/router-name (new-router))) + (routes [_] (reitit/routes (new-router))) + (compiled-routes [_] (reitit/compiled-routes (new-router))) + (options [_] (reitit/options (new-router))) + (route-names [_] (reitit/route-names (new-router))) + (match-by-path [_ path] (reitit/match-by-path (new-router) path)) + (match-by-name [_ name] (reitit/match-by-name (new-router) name)) + (match-by-name [_ name path-params] (reitit/match-by-name (new-router) name path-params)))) diff --git a/src/lambdaisland/souk/json_ld.clj b/src/lambdaisland/souk/json_ld.clj index 1e95611..e6abff3 100644 --- a/src/lambdaisland/souk/json_ld.clj +++ b/src/lambdaisland/souk/json_ld.clj @@ -1,4 +1,6 @@ (ns lambdaisland.souk.json-ld + "Interfacing with JSON-LD endpoints, and converting to and from idiomatic + Clojure data." (:require [hato.client :as hato] [clojure.string :as str] [clojure.walk :as walk])) diff --git a/src/lambdaisland/souk/setup.clj b/src/lambdaisland/souk/setup.clj new file mode 100644 index 0000000..72e3a99 --- /dev/null +++ b/src/lambdaisland/souk/setup.clj @@ -0,0 +1,44 @@ +(ns lambdaisland.souk.setup + (:require [lambdaisland.webbing.config :as config] + [clojure.java.io :as io])) + +(def project 'lambdaisland/souk) +(def start-keys #{:http/server}) + +(def schemas + {:settings [[:port int?] + [:dev/reload-routes? boolean?]] + :secrets []}) + +(defn proj-resource [path] + (io/resource (str project "/" path))) + +(defn prod-setup [] + {:schemas schemas + :keys start-keys + :sources {:config [(proj-resource "config.edn")] + :secrets [(config/cli-args) + (config/env)] + :settings [(config/cli-args) + (config/env) + (config/default-value) + (proj-resource "settings-prod.edn") + (proj-resource "settings.edn")]}}) + +(defn dev-setup [] + (let [local-file (io/file "config.local.edn") + local-config (when (.exists local-file) + (read-string (slurp local-file)))] + {:schemas schemas + :keys (:dev/start-keys local-config start-keys) + :sources {:config [(proj-resource "config.edn") + (proj-resource "config-development.edn") + (dissoc local-config :dev/start-keys)] + :secrets [(config/dotenv) + (config/env) + (config/default-value)] + :settings [(config/dotenv) + (config/env) + (config/default-value) + (proj-resource "settings-dev.edn") + (proj-resource "settings.edn")]}}))